kuban-forum.ru - Лучший форум для общения

Информация о пользователе

Привет, Гость! Войдите или зарегистрируйтесь.


Вы здесь » kuban-forum.ru - Лучший форум для общения » 🍰Кофейня » Новости форума и обсуждение его работы


Новости форума и обсуждение его работы

Сообщений 91 страница 120 из 131

91

Добавлен мультитрековый музыкальный плеер в постах (кнопка в виде наушников).


[{n:"Королёк-птичка певчая - из фильма",u:"https://forumstatic.ru/files/001a/f0/7d/29851.mp3?v=1",c:"https://upforme.ru/uploads/001a/f0/7d/2/54385.png"}]


Внимание!

Этому скрипту требуются прямые ссылки на музыкальные файлы и файлы обложки.

И если с прямыми ссылками на картинки проблем в общем нет, то как наиболее просто искать или делать прямые ссылки на музыкальные файлы, я пока что не готов сказать :)

P.S. Если у вас после вставки ссылок вместо плеера отображается какой-то текст, попробуйте обновить страничку.

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

92

Добавлены смайлики поздравлений, а также смайлики для серьёзных новостей :)

https://upforme.ru/uploads/001a/f0/7d/2/909404.webp
https://upforme.ru/uploads/001a/f0/7d/2/923711.png

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

93

В верхнее меню форума (самое верхнее:) ) добавлена ссылка на страничку с донатами на Boosty.
Конечно, никто подписываться не собирается, но пускай будет :)

https://upforme.ru/uploads/001a/f0/7d/2/70785.jpg

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

94

Для вставки видео на форум используются сторонние сервисы. Для вставки видео через видеохостинги есть кнопка на панели быстрого ответа.

Сейчас к ним, дополнительно также можно использовать сервис Sendvid Как вставить видео на форум

Плюсы: Это  быстро работает, и использовать легко и удобно, в том числе, при вставке коротких видео с мобильного телефона.

Минусы: Надёжность этого сервиса определённо ниже, чем у глобальных сайтов типа TouTube, поэтому видео  имеет смысл вставлять не очень ценные, т.е. такие, какие, например, вы выкладываете в "сторис" в Ватсап и Инстаграм.

❗Ваши  видео могут быть удалены автоматически, если они неактивны в течение 90 дней подряд.
Пока есть просмотр  хотя бы раз в 90 дней, ваши видео остаются.

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

95

В мини-профиль (тот, что под аватаром слева), под спойлер (с синими стрелочками) добавлен счётчик количества тем, созданных форумчанином.

Слова "Создано тем:" и "Сообщений:" теперь гиперссылки на списки тем и сообщений соответственно.

P.S. Счётчик количества тем начинает считать с момента установки, поэтому текущее количество надо задать или откорректировать. Я постепенно постараюсь вписать эти циферки в профили каждому, но если вы хотите ускорить процесс, то посчитайте сами свои темы и напишите мне их количество:)

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

96

Появилась возможность для всех желающих поставить себе личный статус, это небольшая надпись над аватаркой.
устанавливается в профиле в пункте Дополнительно.

https://upforme.ru/uploads/001a/f0/7d/57/t575185.png

Получаем вот такой результат:

https://upforme.ru/uploads/001a/f0/7d/57/t839135.jpg

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

97

К ссылкам в мини-профиле (👤Профиль ✉️ ЛС 📧E-mail) и к элементам управления постом (❌ Удалить  ✍️ Редактировать  💬 Цитировать), а также к элементам в выпадающем списке мини-профайла (спойлер под синими стрелочками)  добавлены эмодзи, что улучшает визуальное восприятие.

Эти изменения нормально отображаются как при просмотре, так и при постинге. В мобильной версии это тоже работает.

Цвета в дополнительных надписи в мини-профайле  и в цветах ников (характерно,  например,  для модераторов и админов) теперь также правильно отображаются при постинге.

Раньше все изменения правильно отображались только при перезагрузке страницы.

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

98

Добавлена возможность делать слайдеры с картинками.
Функционал форума: Слайдер с картинками

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

99

Насчет нового решения для списка активных тем, что в самом верху главной страницы. Видел, что идет работа, не вмешивался. походу она немного утихла. Немного покатал, посмотрел... есть замечания и предложения. Сразу скажу, что даже не пытался глянуть на мобильную версию, может там тоже не все ладно. Пока - про десктоп.

Первое

Список изначально сортирован неправильно. Новые ответы должны быть вверху, а не внизу. Почему ДОЛЖНЫ?
Да потому что люди нынче пишут и слева направо и наоборот, но ВСЕГДА ТЕКСТ ИДЕТ СВЕРХУ ВНИЗ.
ИМХО, на то же нацелены и поисковые роботы - чем выше фраза, тем выше ее рейтинг.

Второе.

Маленький тайм-аут после события hover - темы опять прокручиваются в исходный вид.
Информативность отличная, но из-за малого времени (см. предыдущий пункт) она не реализуется. Нет времени прочитать ВСЁ. Даже в этом блоке, а не то чтоб на форуме (шутка).

Если у кого есть что добавить-возразить-поправить, я совсем не против. Думаю, и автор этих новшеств тоже. Мы все ценим callback.

Подпись автора

"Я не собираюсь расшифровывать, шиза это или такое чувство юмора. По умолчанию - шиза."(С)

0

100

Итак, в последние несколько дней я был занят функционалом списка тем на главной странице, который, кроме списка собственно тем, дополнен ещё и списком сообщений, и всё это имеет вид live-box (т.е. "живое" обновление).

Live-box — это скрипт, который автоматически обновляет список последних сообщений на главной странице форума в реальном времени. Он загружает новые темы, показывает последние сообщения, подсвечивает новые сообщения и отслеживает изменения.

Основные функции:
✅ Автозагрузка тем — раз в 7 секунд.
✅ Автозагрузка сообщений — при появлении новых тем.
✅ Перепроверка сообщений — раз в 20 секунд.
✅ Подсветка новых сообщений — фиолетовым фоном.
✅ Очистка лишних элементов — цитат, спойлеров, подписей.
✅ Сохранение состояния паузы — в localStorage.
✅ Обработка ошибок — до 10 попыток.
✅ Управление отображением через профиль пользователя.

В профиле можно отключить отображение списка тем и сообщений на главной странице.
https://upforme.ru/uploads/001a/f0/7d/2/640201.webp

В мобильном варианте live-box  отображается в упрощённом виде - только список тем.

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

101

29420,374 написал(а):

Первое

Список изначально сортирован неправильно. Новые ответы должны быть вверху, а не внизу. Почему ДОЛЖНЫ?
Да потому что люди нынче пишут и слева направо и наоборот, но ВСЕГДА ТЕКСТ ИДЕТ СВЕРХУ ВНИЗ.
ИМХО, на то же нацелены и поисковые роботы - чем выше фраза, тем выше ее рейтинг.

Если текст идёт сверху вниз, то самое последнее написанное где находится? Правильно,  внизу :) Откройте любую книгу, там новый текст тоже появляется, как ни  странно, не сверху и снизу :)

Это всё вопрос привычки.
Объективно говоря, когда в списке тем в разделах он отсортирован "новые вверху", это удобно, т.к. не нужно прокручивать список вниз. Так что тут, безусловно есть некое противоречие с порядком отображения тем в лайв-боксе, но лично мне это в целом удобно :) В принципе, можно сделать переключатель на обратное расположение тем с сообщениями.

29420,374 написал(а):

Второе.

Маленький тайм-аут после события hover - темы опять прокручиваются в исходный вид.
Информативность отличная, но из-за малого времени (см. предыдущий пункт) она не реализуется. Нет времени прочитать ВСЁ. Даже в этом блоке, а не то чтоб на форуме (шутка).

Вы, я так понимаю, хотите, чтобы прокрутка полностью останавливалась, если курсор находится где-либо внутри  блока Live-бокса. Это классическая проблема "залипания" (scroll hijacking) и она в данный момент не реализована.




Возможно, в будущем я что-то добавлю в функционал из вышеперечисленных предложений. Но обещать не буду, т.к. это значительные затраты личного времени, а объём кода составляет сотни строк (в несжатом виде около 900  строк) со сложной логикой, и добавление любого нового функционала в данном означает несколько часов ловли багов, что чем-то напоминает труд Сизифа :)

Поэтому, не сочтите за дерзость, если кого-то в целом не устраивает текущая реализация списка "живых" тем с сообщениями, то он может её отключить в своём профиле :)

P.S. Чтобы список сообщений не "убегал" при просмотре, можно его прокручивать и держать мышкой за скроллер (полосу прокрутки) в самом лайв боксе:)

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

102

29443,2 написал(а):

если кого-то в целом не устраивает

не-не-не-не-не! (С)"Не шутите с Зоханом".

Это были не претензии. Это было мое мнение. ни к чему не обязывающее. Ни разработчика, ни меня.

Есть другие отклики? :)

Подпись автора

"Я не собираюсь расшифровывать, шиза это или такое чувство юмора. По умолчанию - шиза."(С)

0

103

29444,374 написал(а):

Есть другие отклики? :)

Ждём:)

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

104

29445,2 написал(а):

Ждём:)

Они сами не появятся. Их надо "выпрашивать". Ну, устроить опрос. Ну, не пускать на форум, пока не ответишь :) Ну... поищи ответы как получить обратную связь.

Я знаю беспроигрышный способ. Заключается он в том, чтоб сделать так, как тебе прям сейчас нравится (не факт, что будет нравиться вечно). И сделать этот режим неизменным.

А потом спросить мнение...

ЗЫ: я так по умолчанию включил музон на своем сайте. Оркестровка, легкая... но сколько же говна наслушался... самое легкое "а где кнопка выключить?".

Подпись автора

"Я не собираюсь расшифровывать, шиза это или такое чувство юмора. По умолчанию - шиза."(С)

0

105

29448,374 написал(а):

ЗЫ: я так по умолчанию включил музон на своем сайте. Оркестровка, легкая... но сколько же говна наслушался... самое легкое "а где кнопка выключить?".

Фоновая музыка - это общеизвестный "дурной тон" :) Незапрашиваемое поведение ибо. Хуже фоновой музыки только "ку-ку" и прочий внезапный контент на сайте Леонида Каганова. Зачем он сделал этот совершенно дебильный "спецэффект", я не постигаю.
И, конечно, кнопка отключения всевозможных звуков должна быть.

Кстати, с кнопкой "выключить" у списка "живых тем" всё в порядке, и она даже работает :)

Подпись автора

Как вставить видео на форум Слайдер для картинок

0

106

29451,57 написал(а):

Кстати, с кнопкой "выключить" у списка "живых тем"

У меня зрение не айс (иной раз курсор на экране не вижу), так что я ее не видел и не искал. Ты опять "переходишь на личности" :) Т.е. отвечаешь на мелочи от одного из пользователей. Не пытаясь учить, я скромно замечу, что нужна более обширная база откликов на изменение дизайна и функций. Уж как ты это обеспечишь (если захочешь) - не моя задача.

Спасибо за подсказку, но я нашел другое решение - переключаюсь на "Активные темы". Там приходится работать правой иышки, но это уже мелочи.

Подпись автора

"Я не собираюсь расшифровывать, шиза это или такое чувство юмора. По умолчанию - шиза."(С)

0

107

🔥Итак, список тем и сообщений на главной странице в  виде 🎁live-box (т.е. "живое" обновление).

🎁Live-box — это программа (скрипт), которая автоматически 🔀обновляет список последних сообщений на главной странице форума в "реальном времени". Он загружает новые темы, показывает последние сообщения, подсвечивает новые сообщения и отслеживает изменения.

🛠️Основные функции:
✅ 🔁Автозагрузка тем — раз в 7 секунд.
✅ 🔁Автозагрузка сообщений — при появлении новых тем.
✅ ☑️Перепроверка сообщений — раз в 20 секунд.
✅ 💡Подсветка новых сообщений — фиолетовым фоном.
✅ 🧹Очистка лишних элементов — цитат, спойлеров, подписей.
✅ 🚩Обработка ошибок — до 10 попыток.
✅ 💾Сохранение состояния ⏸️паузы — в localStorage устройства.
✅ 💾Сохранение настроек отображения — в localStorage устройства.
✅⚙️Управление отображением через профиль пользователя.
✅🌸Адаптивный дизайн под разные типы устройств (🖥️десктоп, 💻ноутбук, 📔планшет, 📱телефон).
✅✔️Полная поддержка 👨🏿тёмной темы.


В Вашем 🐸профиле можно:

  • ❌Отключить отображение списка тем и сообщений на главной странице

  • 📝Установить длину тем и сообщений в 🎁live-box.

👀Посмотреть картинку🖼️

https://upforme.ru/uploads/001a/f0/7d/2/407341.webp

В 📱мобильном варианте просмотра live-box  отображается в упрощённом виде: только список тем.
Настройка отображения 🎁live-box в 🐸профиле при 📱мобильном варианте просмотра: 📐длина отображаемых тем.

👀Посмотреть картинку🖼️

https://upforme.ru/uploads/001a/f0/7d/2/788205.webp

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

108

Добавлена возможность делать цветные названия тем с цветными фонами под названием.

Особенность: в "цветном" режиме - количество символов в названии темы уменьшается до 48 - красота требует жертв, но есть переключатель, чтобы вернуться в "обычный" режим, там количество символов в названии темы равно 70.

Если вы в "обычном" режиме ввели символов больше, чем 48 и переключились на "цветной", то "лишние" символы автоматически обрезаются.

В лайв-боксе с темами раскрашенные темы также отображаются.

❗В мобильном виде  - цветные темы видно, но создать их ❌нельзя (т.к. палитры будут мелковаты).

P.S. Раскраска тем сейчас включена везде, кроме раздела "Политический форум".

P.P.S. Не злоупотребляйте  https://forumstatic.ru/img/smilies/MyBB/light/smile.gif

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

109

👁Превью темы и профиля

🚩 Возможности: предпросмотр темы и профиля (без захода в тему)

✅ 👁 Превью при наведении на ссылку темы или профиля
✅ 🏼🟨Можно просмотреть первое и последнее сообщение в теме  с переключением (как в Invision Community)
✅ 🌓 Полная поддержка тёмной темы (body.dark-theme) 
✅ 👤 Профиль пользователя: аватар, группа, статистика, дата регистрации 
✅ 📱 Отключено на мобильных — не мешает навигации 

🖼️ Скриншоты:

Превью профиля

https://upforme.ru/uploads/001a/f0/7d/2/92199.jpg

Превью темы

https://upforme.ru/uploads/001a/f0/7d/2/754160.jpg

✅ Всплывающие окошки привью убираются по щелчку за их пределами, либо сами через 2 секунды.
✅ Если курсор мыши находится в окошке, то отсчёт 2 секунд идёт после убирания курсора.

🔥Это самое красивое, надёжное и функциональное превью по эту сторону Миссисиппи на этом движке форума :)

(защиту от сбоев при доступе на сервер, как в лайв-боксе на главной странице, я не делал, т.к. это же всего лишь превью, можно убрать мышку и навести заново :) )

Для администраторов форумов:

Краткие особенности реализации

● Скрипт при предпросмотре темы показывает содержание  темы, при этом максимально удобно показывается текстовый  и графический контент.

● Картинки и видео в теме - масштабируются по размеру окошка привью.

● Заменяется на заглушки контент между некоторыми тегами. Смысл - обеспечение полной безопасности (для html), невозможность адекватно отобразить элемент на маленьком окошке (для table), исключение показа  заведомо «тяжёлого» контента (для media).

● Ники и группы пользователей, и их атрибуты  - везде запрашиваются только через API сервера.

● Для элементов «Цитата», «Код» и «Спойлер» сконструированы выпадающие списки , т. н. «аккордеоны», с поддержкой и раскраской вложенности.

● Установлен максимальный уровень вложенности (MAX_NESTING_LEVEL = 10) для ускорения обработки элементов с очень большой вложенностью, после этого уровня выводится заглушка [Вложенные элементы].

● Всё это даёт независимость работы привью от любых «улучшений» на форуме внешнего вида цитат, спойлеров, блоков с кодом.

Исходные коды: CSS
Код:
/* ============================================== */
/* 👁🗨 Превью темы и профиля */
/* ============================================== */

.tooltipsy {
    padding: 10px;
    background-color: #fff;
    color: #333;
    font-size: 13px;
    position: absolute;
    z-index: 100000;
    box-shadow: 0 4px 20px rgba(0,0,0,0.12);
    border-radius: 8px;
    display: none;
    pointer-events: auto;
    min-width: 250px;
    border: 1px solid #e0e0e0;
    width: auto;
}

/* 🔺 Треугольник указателя */
.tooltipsy:after {
    content: '';
    position: absolute;
    bottom: -10px;
    left: 50%;
    margin-left: -8px;
    border: 8px solid transparent;
    border-top-color: #fff;
}

/* 📏 Общие размеры и прокрутка */
.tooltipsy.topic {
    max-height: 280px;
}
.tooltipsy .scrollable-content {
    height: 150px;
    overflow-y: auto;
    overflow-x: hidden;
    padding-right: 6px;
    margin-top: 10px;
    box-sizing: border-box;
}

/* 🌈 Нижняя акцентная полоса (только для тем) */
.tooltipsy.topic::before {
    content: '';
    position: absolute;
    bottom: 0;
    left: 0; right: 0;
    height: 3px;
    background: linear-gradient(90deg, #3a5f8a, #4a7bb5);
    border-radius: 0 0 8px 8px;
}

/* ============================================== */
/* 👤 Профиль пользователя */
/* ============================================== */

/* 🖼️ Аватары */
.tooltipsy .avatar-img {
    width: 52px;
    height: 52px;
    object-fit: contain;
    background-color: #f8f8f8;
    border: 1px solid #ddd;
    border-radius: 6px;
}
.tooltipsy .avatarTop .avatar-img {
    width: 32px;
    height: 32px;
}
.tooltipsy .userInfo,
.tooltipsy .avatarTop {
    display: flex;
    align-items: flex-start;
    gap: 10px;
}
.tooltipsy .avatarTop { margin-bottom: 6px; }

/* 🧾 Имя и статус */
.tooltipsy .userTitle h3,
.tooltipsy .userTitle h4 {
    margin: 0;
    font-size: 14px;
}
.tooltipsy .userTitle h4 {
    font-weight: bold;
    background-color: #f0f0f0;
    color: inherit;
    padding: 3px 8px;
    border-radius: 4px;
    display: inline-block;
    margin-top: 2px;
}

/* 🎨 Цвета групп пользователей */
.tooltipsy .usTitle-1 { color: #008080 !important; }
.tooltipsy .usTitle-2 { color: #993232 !important; }
.tooltipsy .usTitle-3 { color: #000 !important; }
.tooltipsy .usTitle-4 { color: #072387 !important; }

/* 📝 Дополнительная информация */
.tooltipsy .userBlurb p,
.tooltipsy .lastActivity p {
    margin: 0;
    word-spacing: 8px;
    font-size: 12px;
}
.tooltipsy hr {
    margin: 8px 0;
    border: none;
    height: 1px;
    background-color: #eee;
}
.tooltipsy .datepost {
    display: block;
    font-size: 12px;
    color: #555;
}

/* ============================================== */
/* 💬 Контент темы */
/* ============================================== */

.tooltipsy .topicStart {
    margin-top: 8px;
    line-height: 1.5;
    font-size: 13px;
}

/* 🖼️ Масштабирование изображений в контенте (но не аватаров) */
.tooltipsy .post-content img,
.tooltipsy .post-content a img,
.tooltipsy img.postimg,
.tooltipsy a img.postimg {
    max-width: 100% !important;
    height: auto !important;
    max-height: none !important;
    width: auto !important;
}

/* 🛡️ Исключение: аватар автора в предпросмотре темы */
.tooltipsy .avatarTop img {
    width: 32px !important;
    height: 32px !important;
    max-width: 32px !important;
    max-height: 32px !important;
}

/* ============================================== */
/* 📑 Кнопки: Первое / Последнее сообщение */
/* ============================================== */

.tooltipsy .topPreview {
    display: flex;
    gap: 8px;
    margin-bottom: 10px;
}
.tooltipsy .tab {
    flex: 1;
    text-align: center;
    padding: 6px 0;
    background: #f5f9ff;
    cursor: pointer;
    border-radius: 5px;
    font-weight: 600;
    font-size: 12px;
    color: #2c5aa0;
    transition: background 0.2s ease, color 0.2s ease;
    border: 1px solid #d0e0ff;
}
.tooltipsy .tab:hover,
.tooltipsy .tab.active {
    background: #e0ecff;
}

/* ============================================== */
/* 🧩 Кастомные аккордеоны: цитаты, код, спойлеры */
/* ============================================== */

.tooltipsy .preview-accordion {
    margin: 4px 0;
    border: 1px solid #ccc;
    border-radius: 4px;
    background: #fafafa;
}

/* 🧹 Полный сброс отступов внутри аккордеонов */
.tooltipsy .preview-accordion *,
.tooltipsy .preview-accordion *::before,
.tooltipsy .preview-accordion *::after {
    margin: 0 !important;
    padding: 0 !important;
    box-sizing: border-box;
}

/* 📌 Заголовок аккордеона */
.tooltipsy .preview-summary {
    cursor: pointer;
    list-style: none;
    background: #f5f5f5;
    border-bottom: 1px solid #ddd;
    line-height: 1.4;
    font-size: 13px;
    padding: 4px 8px !important; /* убран правый отступ под иконку */
    position: relative;
    outline: none;
}

/* 🧭 Индикатор раскрытия (стрелка вниз/вверх) */
.tooltipsy .preview-summary::after {
    content: "\25BC";
    font-size: 10px;
    color: #777;
    margin-left: 6px;
    transition: transform 0.2s ease;
    display: inline-block;
    transform-origin: center;
}
.tooltipsy .preview-accordion.expanded > .preview-summary::after {
    transform: rotate(180deg);
}

/* 🎨 Цвет фона заголовка при раскрытии/сворачивании */
.tooltipsy .preview-accordion.collapsed > .preview-summary {
    background: #f5f5f5 !important;
    color: #333 !important;
}
.tooltipsy .preview-accordion.expanded > .preview-summary {
    background: #e8f0ff !important;
    color: #072387 !important;
    font-weight: 600 !important;
}

/* 🎯 Цвета по типу блока при раскрытии */
.tooltipsy .preview-accordion[data-type="quote"].expanded > .preview-summary {
    background: #e0ecff;
    border-left: 3px solid #072387;
}
.tooltipsy .preview-accordion[data-type="code"].expanded > .preview-summary {
    background: #e0f8f8;
    border-left: 3px solid #008080;
}
.tooltipsy .preview-accordion[data-type="spoiler"].expanded > .preview-summary {
    background: #f8e0ff;
    border-left: 3px solid #8B008B;
}

/* 🔑 КЛЮЧЕВОЕ ПРАВИЛО: изоляция через прямых потомков */
.tooltipsy .preview-accordion.collapsed > .preview-content {
    display: none !important;
}
.tooltipsy .preview-accordion.expanded > .preview-content {
    display: block !important;
}

/* 📦 Вложенные аккордеоны */
.tooltipsy .preview-content .preview-accordion,
.tooltipsy .preview-accordion .preview-accordion {
    width: 100%;
    margin: 4px 0 !important;
    border-radius: 4px;
}
.tooltipsy .preview-content .preview-accordion .preview-summary,
.tooltipsy .preview-accordion .preview-accordion .preview-summary {
    background: #f7f7f7;
    font-size: 12.5px;
}

/* ============================================== */
/* 💻 Блоки кода */
/* ============================================== */

.tooltipsy pre,
.tooltipsy code {
    display: block;
    width: 100% !important;
    max-width: 100% !important;
    white-space: pre-wrap;
    word-wrap: break-word;
    background: #f4f4f4;
    border: 1px solid #ddd;
    border-radius: 0;
    font-size: 12px;
    line-height: 1.4;
    overflow-x: hidden;
}
.tooltipsy pre {
    padding: 4px 6px !important;
}

/* ============================================== */
/* 🧱 Заглушка для слишком глубокой вложенности */
/* ============================================== */

.tooltipsy .nesting-limit {
    font-style: italic;
    color: #888;
    padding: 4px 8px;
    background: #f9f9f9;
    border-left: 2px solid #ccc;
    font-size: 12px;
}
/* 🚫 Защита: у заглушки не должно быть иконки */
.tooltipsy .nesting-limit::after {
    display: none !important;
}

/* ============================================== */
/* 🌙 Тёмная тема */
/* ============================================== */

/* 🎨 Основной стиль */
body.dark-theme .tooltipsy {
    background-color: var(--title);
    color: var(--white-7);
    border: 1px solid var(--black-2);
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
body.dark-theme .tooltipsy:after {
    border-top-color: var(--title);
}
body.dark-theme .tooltipsy hr {
    background-color: var(--black-2);
}

/* 👤 Профиль — текст и аватар */
body.dark-theme .tooltipsy .datepost,
body.dark-theme .tooltipsy .userBlurb p,
body.dark-theme .tooltipsy .lastActivity p {
    color: var(--white-4);
}
body.dark-theme .tooltipsy .userTitle h3 {
    color: var(--white-8);
}
body.dark-theme .tooltipsy .userTitle h4 {
    background-color: rgba(255,255,255,0.12);
}
body.dark-theme .tooltipsy .avatar-img {
    background-color: var(--bg2);
    border-color: var(--black-2);
}

/* 📑 Вкладки */
body.dark-theme .tooltipsy .tab {
    background: rgba(40,46,56,0.6);
    color: var(--white-7);
    border: 1px solid var(--black-2);
}
body.dark-theme .tooltipsy .tab:hover,
body.dark-theme .tooltipsy .tab.active {
    background: rgba(53,61,75,0.8);
    color: var(--white-8);
}

/* 🧩 Аккордеоны — тёмная тема */
body.dark-theme .tooltipsy .preview-accordion {
    background: rgba(255,255,255,0.03);
    border-color: var(--black-2);
}
body.dark-theme .tooltipsy .preview-summary {
    background: rgba(255,255,255,0.06);
    color: var(--white-7);
}
body.dark-theme .tooltipsy .preview-accordion.expanded .preview-summary {
    background: rgba(255,255,255,0.1);
}
body.dark-theme .tooltipsy .preview-summary::after {
    color: var(--white-6) !important;
}

/* 🎨 Цвета заголовков аккордеонов в тёмной теме */
body.dark-theme .tooltipsy .preview-accordion.collapsed > .preview-summary {
    background: rgba(255,255,255,0.06) !important;
    color: var(--white-7) !important;
}
body.dark-theme .tooltipsy .preview-accordion.expanded > .preview-summary {
    background: rgba(65, 105, 225, 0.15) !important; /* тёмно-синий акцент */
    color: #a0c0ff !important;
}

/* 🔗 Ссылки и скроллбар */
body.dark-theme .tooltipsy a {
    color: #a3c0d3;
    text-decoration: underline;
}
body.dark-theme .tooltipsy a:hover {
    color: #e0f0ff;
}
body.dark-theme .tooltipsy .scrollable-content::-webkit-scrollbar {
    width: 8px;
}
body.dark-theme .tooltipsy .scrollable-content::-webkit-scrollbar-thumb {
    background-color: rgba(200,200,200,0.4);
    border-radius: 4px;
}
body.dark-theme .tooltipsy .scrollable-content::-webkit-scrollbar-track {
    background-color: rgba(0,0,0,0.1);
    border-radius: 4px;
}

/* 💻 Код в тёмной теме */
body.dark-theme .tooltipsy pre,
body.dark-theme .tooltipsy code {
    background: rgba(255,255,255,0.05);
    border-color: var(--black-2);
    color: var(--white-8);
}
Исходные коды: JS
Код:
(function($) {
    // 📱 Отключаем превью на мобильных устройствах (ширина ≤ 768px)
    if (window.screen.width <= 768) return;

    // 🧱 Максимальный уровень вложенности аккордеонов (защита от бесконечной рекурсии)
    const MAX_NESTING_LEVEL = 10;

    // 🔍 Извлекает ID из URL (например, из ?id=123 или &id=456)
    function getJid(href) {
        const match = href.match(/[?&]id=(\d+)/);
        return match ? match[1] : null;
    }

    // 🖼️ Возвращает URL аватара по умолчанию
    function defaultAvatar() {
        return 'https://forumstatic.ru/files/0000/14/1c/20038.jpg';
    }

    // 🧠 Глобальные переменные состояния
    let activeTooltip = null;      // Текущий активный тултип
    let hideTimer = null;          // Таймер автоскрытия
    let isClosing = false;         // Флаг: идёт ли закрытие тултипа
    let pendingTarget = null;      // Цель, ожидающая отображения (если тултип занят)
    let scrollHandler = null;      // Обработчик скролла для позиционирования

    // ⏳ Планирует скрытие тултипа через 2 секунды бездействия
    function scheduleHide() {
        if (hideTimer) clearTimeout(hideTimer);
        hideTimer = setTimeout(() => {
            hideTooltip();
        }, 2000);
    }

    // 🚪 Скрывает текущий тултип с плавным исчезновением
    function hideTooltip() {
        if (!activeTooltip || isClosing) return;
        isClosing = true;

        // Отключаем обработчик скролла
        if (scrollHandler) {
            $(window).off('scroll', scrollHandler);
            scrollHandler = null;
        }

        activeTooltip.stop(true, true).fadeOut(200, function() {
            $(this).remove();              // Удаляем DOM-элемент
            activeTooltip = null;          // Сбрасываем ссылку
            isClosing = false;             // Разрешаем новые действия
            if (hideTimer) clearTimeout(hideTimer);
            hideTimer = null;

            // Если есть отложенная цель — показываем её
            if (pendingTarget) {
                const { $link, href, Jid } = pendingTarget;
                pendingTarget = null;
                showPreview($link, href, Jid);
            }
        });
    }

    // 📍 Обновляет позицию тултипа относительно целевого элемента
    function updatePosition($tip, $target) {
        if (!activeTooltip) return;
        const targetPos = $target.offset();
        const tipHeight = $tip.outerHeight();
        const tipWidth = $tip.outerWidth();
        const targetCenter = targetPos.left + $target.outerWidth() / 2;

        // Горизонтальное позиционирование: центрируем по цели
        let left = targetCenter - tipWidth / 2;
        const winWidth = $(window).width();
        if (left < 10) left = 10; // отступ слева
        if (left + tipWidth > winWidth - 10) left = winWidth - tipWidth - 10; // отступ справа

        // Для тем — смещаем вправо (лучше читается)
        if ($tip.hasClass('topic')) {
            const ideal = targetCenter + 20;
            if (ideal + tipWidth <= winWidth - 10 && ideal >= 10) {
                left = ideal;
            }
        }

        // Вертикальное позиционирование: над элементом
        const top = targetPos.top - tipHeight - 8;
        const safeTop = Math.max(10, top); // не выше 10px от верха

        $tip.css({ left: left, top: safeTop });
    }

    // 👁️‍🗨️ Создаёт и показывает превью (профиль или тема)
    function showPreview($link, href, Jid) {
        // Если тултип уже активен — ставим задачу в очередь
        if (activeTooltip || isClosing) {
            pendingTarget = { $link, href, Jid };
            return;
        }

        // Определяем тип ссылки
        const isProfile = href && href.includes('profile.php');
        const isTopic = Jid && !isProfile; // если ID есть, и это не профиль — считаем темой
        if (!isProfile && !isTopic) return;

        // Создаём контейнер тултипа
        const $tip = $(`<div class="tooltipsy ${isProfile ? 'profile' : 'topic'}">Загрузка...</div>`)
            .appendTo('body')
            .css({ display: 'none' });

        activeTooltip = $tip;

        // 🔁 Загрузка и обработка контента
        const loadContent = () => {
            if (!activeTooltip) return;

            // 📅 Форматирует timestamp в читаемую дату (ru-RU)
            const formatDate = (ts) => {
                return new Date(ts * 1000).toLocaleString('ru-RU', {
                    day: '2-digit',
                    month: '2-digit',
                    year: 'numeric',
                    hour: '2-digit',
                    minute: '2-digit'
                }).replace(/,/g, ' в ');
            };

            // 🧼 Очищает и преобразует сообщение для безопасного отображения
            const processMessage = (msg) => {
                if (!msg) return '[пусто]';

                // Заменяем BB-теги на заглушки
                let safeMsg = msg
                    .replace(/\[html\][\s\S]*?\[\/html\]/gi, '[HTML контент]')
                    .replace(/\[table\][\s\S]*?\[\/table\]/gi, '[Таблица]')
                    .replace(/\[media\][\s\S]*?\[\/media\]/gi, '[Медиа]');

                // Заменяем <details> на обычные div (чтобы не конфликтовали)
                safeMsg = safeMsg.replace(/<details\b[^>]*>/gi, '<div class="old-details">');
                safeMsg = safeMsg.replace(/<\/details>/gi, '</div>');

                const temp = $('<div>').html(safeMsg);

                // 🎨 Стили для разных типов блоков
                const TAG_STYLES = {
                    quote:    { icon: '📌', color: '#072387', bg: '#d0e0ff' },
                    code:     { icon: '💻', color: '#008080', bg: '#d0f0f0' },
                    spoiler:  { icon: '🔒', color: '#8B008B', bg: '#f0d0ff' },
                    html:     { icon: '🖼️', color: '#d32f2f', bg: '#ffe0e0' },
                    table:    { icon: '📊', color: '#2e7d32', bg: '#e8f5e9' },
                    media:    { icon: '🎬', color: '#ad1457', bg: '#fce4ec' }
                };

                // 💅 Генерирует inline-стиль для заголовка блока
                const getTagStyle = (type) => {
                    const s = TAG_STYLES[type];
                    return `color: ${s.color}; background-color: ${s.bg}; padding: 0 4px; border-radius: 3px; font-weight: bold;`;
                };

                // 🔁 Рекурсивная обработка вложенных блоков (.quote-box, .code-box, .spoiler-box)
                function processBlocks($element, level = 0) {
                    // Защита от слишком глубокой вложенности
                    if (level >= MAX_NESTING_LEVEL) {
                        $element.replaceWith('<div class="nesting-limit">[Вложенные элементы]</div>');
                        return;
                    }

                    // Находим все поддерживаемые блоки
                    $element.find('.quote-box, .code-box, .spoiler-box').each(function() {
                        const $block = $(this);
                        const tagName = $block.hasClass('quote-box') ? 'quote' :
                                        $block.hasClass('code-box') ? 'code' : 'spoiler';

                        // Удаляем <cite> из цитат
                        if (tagName === 'quote') {
                            $block.children('cite').remove();
                        }

                        // Извлекаем заголовок спойлера
                        let title = 'Спойлер';
                        if (tagName === 'spoiler') {
                            const $titleEl = $block.children('.spoiler-title').first();
                            if ($titleEl.length) {
                                title = $titleEl.text().trim() || 'Спойлер';
                                $titleEl.remove();
                            }
                        }

                        // 🧩 Создаём кастомный аккордеон
                        const $accordion = $('<div class="preview-accordion collapsed"></div>');
                        $accordion.attr('data-type', tagName); // ← для стилизации по типу
                        const $summary = $(`<div class="preview-summary" style="${getTagStyle(tagName)}">${TAG_STYLES[tagName].icon} [${tagName === 'quote' ? 'Цитата' : tagName === 'code' ? 'Код' : `Спойлер: ${title}`}]</div>`);
                        const $content = $('<div class="preview-content"></div>');

                        // Переносим содержимое внутрь аккордеона
                        $content.append($block.contents());
                        $accordion.append($summary).append($content);
                        $block.replaceWith($accordion);

                        // Рекурсивно обрабатываем вложенные блоки
                        processBlocks($content, level + 1);
                    });
                }

                processBlocks(temp, 0);

                // 🖼️ Масштабируем медиа в контенте
                temp.find('img, video, iframe, table, pre').css({
                    'max-width': '100%',
                    'height': 'auto',
                    'box-sizing': 'border-box'
                });
                temp.find('table').css('table-layout', 'fixed');

                let finalHtml = temp.html();

                // Заменяем заглушки на стилизованные теги
                const replacements = [
                    { from: /\[HTML контент\]/g, type: 'html' },
                    { from: /\[Таблица\]/g, type: 'table' },
                    { from: /\[Медиа\]/g, type: 'media' }
                ];

                replacements.forEach(({ from, type }) => {
                    const s = TAG_STYLES[type];
                    const label = type === 'html' ? 'HTML контент' : type === 'table' ? 'Таблица' : 'Медиа';
                    const styledTag = `<span style="color: ${s.color}; background-color: ${s.bg}; padding: 0 4px; border-radius: 3px; font-weight: bold;">${s.icon} [${label}]</span>`;
                    finalHtml = finalHtml.replace(from, styledTag);
                });

                // 📌 Заменяем упоминания на заглушки с data-атрибутом
                finalHtml = finalHtml.replace(/(\d+),(\d+)\s+написал\(а\):/g, '<span class="mention-placeholder" data-post-id="$2">Загрузка...</span>');

                return finalHtml;
            };

            // 👤 Загрузка профиля
            if (isProfile) {
                $.getJSON('/api.php?method=users.get&user_id=' + Jid + '&fields=username,group_id,group_title,avatar,registered,sex,age,num_posts,last_visit,respect_plus')
                    .done(function(j) {
                        const u = j?.response?.users?.[0];
                        if (u) {
                            const sex = { '2': 'Женский', '1': 'Мужской', '0': 'Неизвестно' }[u.sex] || 'Неизвестно';
                            const lastVisit = formatDate(u.last_visit);
                            const registered = new Date(u.registered * 1000).toLocaleString('ru-RU').split(',')[0];
                            const avatar = u.avatar || defaultAvatar();
                            const groupClass = u.group_id ? `usTitle-${u.group_id}` : 'usTitle-4';

                            const html = `
                                <div class="member">
                                    <div class="userInfo">
                                        <img src="${avatar}" alt="" class="avatar-img">
                                        <div class="userTitle">
                                            <h3 class="username">${u.username}</h3>
                                            <h4 class="${groupClass}">${u.group_title}</h4>
                                        </div>
                                    </div>
                                    <hr>
                                    <div class="userBlurb">
                                        <span>Пол: <strong>${sex}</strong></span>
                                        <span>Возраст: <strong>${u.age || '—'}</strong></span><br>
                                        <span class="userStats">
                                            <p>На форуме с: <strong>${registered}</strong></p>
                                            <p>Сообщения: <strong>${u.num_posts}</strong></p>
                                            <p>Уважение: <strong class="reit">${u.respect_plus}</strong></p>
                                        </span>
                                    </div>
                                    <hr>
                                    <span class="lastActivity">
                                        <p>Последняя активность: <strong>${lastVisit}</strong></p>
                                    </span>
                                </div>
                            `;
                            $tip.html(html);
                            finalizeTooltip($tip, $link, isProfile);
                        } else {
                            $tip.html('Профиль не найден');
                            finalizeTooltip($tip, $link, isProfile);
                        }
                    })
                    .fail(() => {
                        $tip.html('Ошибка загрузки профиля');
                        finalizeTooltip($tip, $link, isProfile);
                    });
            } 
            // 💬 Загрузка темы (первое и последнее сообщение)
            else {
                const htmlButtons = `
                    <div class="topPreview">
                        <div class="tab active" data-target="first">Первое сообщение</div>
                        <div class="tab" data-target="last">Последнее сообщение</div>
                    </div>
                    <hr>
                    <div class="scrollable-content">
                        <div class="post-content" style="display:block;">
                            <div class="avatarTop">
                                <img src="${defaultAvatar()}" alt="" class="avatar-img">
                                <span class="datepost">Загрузка...</span>
                            </div>
                        </div>
                    </div>
                `;
                $tip.html(htmlButtons);

                // Измеряем ширину тултипа для корректного позиционирования
                $tip.css({ left: '-9999px', display: 'block' });
                const tipWidth = $tip.outerWidth();
                $tip.css('width', tipWidth);

                // Загружаем первое и последнее сообщение параллельно
                $.when(
                    $.getJSON('/api.php?method=post.get&topic_id=' + Jid + '&limit=1&sort_by=posted&sort_dir=asc&fields=id,username,avatar,message,posted'),
                    $.getJSON('/api.php?method=post.get&topic_id=' + Jid + '&limit=1&sort_by=posted&sort_dir=desc&fields=id,username,avatar,message,posted')
                ).done(function(firstRes, lastRes) {
                    const firstPost = firstRes[0]?.response?.[0];
                    const lastPost = lastRes[0]?.response?.[0];
                    if (!firstPost) {
                        $tip.find('.scrollable-content').html('<div class="post-content">Тема пуста</div>');
                        finalizeTooltip($tip, $link, isProfile);
                        return;
                    }

                    // 🖋️ Рендер одного сообщения
                    const renderPost = (post, isActive = false) => {
                        if (!post) return '';
                        const avatar = post.avatar || defaultAvatar();
                        const date = formatDate(post.posted);
                        const content = processMessage(post.message);
                        const username = post.username || 'Аноним';

                        return `
                            <div class="post-content" style="display:${isActive ? 'block' : 'none'};">
                                <div class="avatarTop">
                                    <img src="${avatar}" alt="" class="avatar-img">
                                    <span class="datepost">🕒 ${date}<br>👤 <strong>${username}</strong></span>
                                </div>
                                <div class="topicStart">${content}</div>
                            </div>
                        `;
                    };

                    const messagesHtml = `
                        ${renderPost(firstPost, true)}
                        ${renderPost(lastPost, false)}
                    `;
                    $tip.find('.scrollable-content').html(messagesHtml);
                    finalizeTooltip($tip, $link, isProfile);
                }).fail(() => {
                    $tip.find('.scrollable-content').html('<div class="post-content">Ошибка загрузки</div>');
                    finalizeTooltip($tip, $link, isProfile);
                });
            }
        };

        loadContent();
    }

    // ✨ Финальная настройка тултипа после загрузки контента
    function finalizeTooltip($tip, $link, isProfile) {
        if (!activeTooltip) return;

        updatePosition($tip, $link);
        $tip.css({ display: 'none' }).fadeIn(200, () => {
            if (!isProfile) {
                // 📑 Переключение между первым и последним сообщением
                $tip.find('.tab').on('click', function() {
                    const target = $(this).data('target');
                    $tip.find('.post-content').hide();
                    $tip.find(`.post-content${target === 'first' ? ':first' : ':last'}`).show();
                    $tip.find('.tab').removeClass('active');
                    $(this).addClass('active');
                    if (hideTimer) clearTimeout(hideTimer); // сбрасываем таймер при взаимодействии
                });

                // 🧩 Обработчик клика по аккордеону: переключаем классы expanded/collapsed
                $tip.on('click', '.preview-summary', function() {
                    const $acc = $(this).closest('.preview-accordion');
                    $acc.toggleClass('expanded collapsed');
                    // ❗ Важно: НЕ используем .toggle(), .show(), .hide() — всё управляется через CSS!
                });

                // 📌 Загрузка имён авторов цитат
                const quoteCites = $tip.find('.topicStart .quote-box > cite');
                const postIds = [];
                const citeMap = [];

                quoteCites.each(function() {
                    const $cite = $(this);
                    const text = $cite.text();
                    const match = text.match(/^(\d+),(\d+)\s+написал/);
                    if (match) {
                        const postId = match[2];
                        postIds.push(postId);
                        citeMap.push({ $cite, postId });
                        $cite.text('Автор цитаты...');
                    }
                });

                if (postIds.length > 0) {
                    const uniquePostIds = [...new Set(postIds)];
                    uniquePostIds.forEach(postId => {
                        $.getJSON(`/api.php?method=post.get&post_id=${postId}&fields=username`)
                            .done(function(data) {
                                const username = data?.response?.[0]?.username || 'Пользователь';
                                citeMap
                                    .filter(item => item.postId === postId)
                                    .forEach(item => {
                                        item.$cite.text(username + ' написал(а):');
                                    });
                            })
                            .fail(() => {
                                citeMap
                                    .filter(item => item.postId === postId)
                                    .forEach(item => {
                                        item.$cite.text('Цитата:');
                                    });
                            });
                    });
                }

                // 🏷️ Загрузка имён в упоминаниях ([user] написал(а))
                const mentionPlaceholders = $tip.find('.topicStart .mention-placeholder');
                const postIdsAll = [];
                const mentionMap = [];

                mentionPlaceholders.each(function() {
                    const $el = $(this);
                    const postId = $el.data('post-id');
                    if (postId) {
                        postIdsAll.push(postId);
                        mentionMap.push({ $el, postId });
                        $el.text('Загрузка...');
                    }
                });

                if (postIdsAll.length > 0) {
                    const uniquePostIds = [...new Set(postIdsAll)];
                    uniquePostIds.forEach(postId => {
                        $.getJSON(`/api.php?method=post.get&post_id=${postId}&fields=username`)
                            .done(function(data) {
                                const username = data?.response?.[0]?.username || 'Пользователь';
                                mentionMap
                                    .filter(item => item.postId == postId)
                                    .forEach(item => {
                                        item.$el.text(username + ' написал(а):');
                                    });
                            })
                            .fail(() => {
                                mentionMap
                                    .filter(item => item.postId == postId)
                                    .forEach(item => {
                                        item.$el.text('Цитата:');
                                    });
                            });
                    });
                }
            }

            // 🕒 Запускаем таймер автоскрытия
            scheduleHide();

            // 🔄 Обновляем позицию при скролле
            scrollHandler = () => updatePosition($tip, $link);
            $(window).on('scroll', scrollHandler);
        });
    }

    // 🔗 Функция для привязки обработчиков к конкретным элементам
    function bindHoverToElement($element) {
        if ($element.hasClass('tooltip-bound')) return;

        let $link = null;
        let href = null;
        let Jid = null;

        // Проверяем, является ли элемент ссылкой
        if ($element.is('a') && $element.is('[href]')) {
            $link = $element;
            href = $element.attr('href');
            Jid = getJid(href);
        }
        // Если элемент — это изображение, ищем родительскую ссылку с ID
        else if ($element.is('img')) {
            const $parentLink = $element.closest('a[href]');
            if ($parentLink.length) {
                href = $parentLink.attr('href');
                Jid = getJid(href);
                if (Jid) {
                    $link = $parentLink;
                }
            }
        }

        if (!$link || !Jid) return;

        const isProfile = href && href.includes('profile.php');
        const isTopic = Jid && !isProfile;
        if (!isProfile && !isTopic) return;

        $link.addClass('tooltip-bound');

        $link.on('mouseenter', function(e) {
            e.stopPropagation(); // Останавливаем всплытие, чтобы не срабатывал на родителях
            pendingTarget = { $link, href, Jid };
            showPreview($link, href, Jid);
        });

        $link.on('mouseleave', function() {
            if (!activeTooltip || !activeTooltip.is(':hover')) {
                pendingTarget = null;
            }
            scheduleHide();
        });
    }

    // 🖱️ Привязываем обработчики ТОЛЬКО к:
    // 1. Ссылкам на темы, которые находятся ВНУТРИ div.tclcon, но НЕ ВНУТРИ span.pagestext И НЕ СО СПЕЦИАЛЬНЫМИ ДЕЙСТВИЯМИ (new)
    // Это означает, что мы ищем ссылки внутри div.tclcon, у которых есть span.acchide рядом,
    // И при этом эти ссылки НЕ находятся внутри .pagestext и НЕ содержат action=new (например, "Новые сообщения").
    $('.tclcon').has('span.acchide').find('a[href*="viewtopic.php"]:not(.pagestext a):not([href*="action=new"])').each(function() {
        bindHoverToElement($(this));
    });

    // 2. Ссылкам на профили (внутри em.user-avatar)
    $('em.user-avatar a').each(function() {
        bindHoverToElement($(this));
    });

    // 🖱️ Скрытие тултипа при клике вне его области
    $(document).on('click', function(e) {
        if (activeTooltip && !$(e.target).closest('.tooltipsy').length) {
            pendingTarget = null;
            hideTooltip();
        }
    });

    // 🐭 Дополнительные обработчики для самого тултипа
    $(document).on('mouseenter', '.tooltipsy', function() {
        if (hideTimer) clearTimeout(hideTimer); // отменяем скрытие при наведении на тултип
    }).on('mouseleave', '.tooltipsy', function() {
        scheduleHide(); // возобновляем таймер при уходе с тултипа
    });
})(jQuery);
            
📚 Подробное описание работы кода

Этот скрипт реализует интерактивные превью (тултипы) при наведении на ссылки на профили и темы на форуме. Работает только на десктопе (отключено на экранах ≤768px).

🔧 Основные компоненты


1. Инициализация и ограничения
● Скрипт не запускается на мобильных устройствах.
● Установлен максимальный уровень вложенности (MAX_NESTING_LEVEL = 10) для защиты от бесконечной рекурсии в цитатах/спойлерах.

2. Извлечение ID из URL
Функция getJid(href) парсит URL вида:

    profile.php?id=123
    viewtopic.php?id=456
 
и возвращает числовой ID. 

3. Система управления тултипами
● Поддерживается только один активный тултип одновременно.
● При попытке открыть новый, пока предыдущий закрывается — цель ставится в очередь (pendingTarget).
● Тултип автоматически скрывается через 2 секунды бездействия (scheduleHide).
● При наведении на тултип — таймер сбрасывается.
● При клике вне тултипа — он закрывается.

4. Позиционирование
● Тултип позиционируется над целевым элементом.
● Для тем — смещается вправо для лучшей читаемости.
● Учитывается ширина окна: тултип не выходит за границы экрана.


👤 Превью профиля

1. Запрашивается API: /api.php?method=users.get&user_id=...

2. Полученные данные:

● аватар (с fallback)
● ник
● группа (через group_id → CSS-класс usTitle-{id})
● пол, возраст, дата регистрации, количество сообщений, уважение, последняя активность

3. Отображается в структурированном виде с разделителями (<hr>).


💬 Превью темы

1. Запрашиваются первое и последнее сообщения параллельно через:
/api.php?method=post.get&topic_id=...&sort_dir=asc
/api.php?method=post.get&topic_id=...&sort_dir=desc
         
● Отображаются вкладки: «Первое сообщение» / «Последнее сообщение».
● Сообщение проходит обработку через processMessage():
● Удаляются или заменяются опасные/неподдерживаемые BB-теги ([html], [table], [media]).
● Заменяются <details> на <div class="old-details">.
● Все вложенные блоки (цитаты, код, спойлеры) рекурсивно преобразуются в кастомные аккордеоны.
● Медиа-элементы масштабируются (max-width: 100%).
● Упоминания и цитаты заменяются на заглушки с последующей подгрузкой имён.


🧩 Кастомные аккордеоны

● Реализованы без использования <details>, чтобы избежать конфликтов со стилями форума.
● Каждый блок (цитата, код, спойлер) оборачивается в:

Код

<div class="preview-accordion collapsed" data-type="quote">
  <div class="preview-summary">📌 [Цитата]</div>
  <div class="preview-content">...</div>
</div>

● Раскрытие/сворачивание управляется только через CSS-классы (expanded / collapsed).
● Рекурсивная обработка вложенных блоков с ограничением глубины.
● При раскрытии — заголовок получает цветовую индикацию по типу (синий для цитат, зелёный для таблиц и т.д.).

Таким образом, этот код — полноценная система интерактивных превью, сочетающая:

● API-интеграцию (запросы к /api.php),
● Безопасную обработку контента,
● Рекурсивную трансформацию вложенных структур,
● Адаптивное позиционирование,
● Поддержку тёмной темы,
● Оптимизацию UX (очередь, таймеры, плавные анимации).
     
Он не использует внешние библиотеки, кроме jQuery, и полностью автономен внутри замыкания.


🎨 Стилизация (CSS)

● Тултипы имеют тень, скругления, указатель (треугольник).
● Для тем — градиентная полоса снизу.
● Аватары масштабируются с object-fit: contain.
● Группы пользователей стилизуются через CSS-классы (usTitle-1, usTitle-2 и т.д.).
● Поддержка тёмной темы через body.dark-theme.
● Все вложенные элементы в аккордеонах обнуляют отступы (margin: 0 !important), чтобы избежать визуального "наложения".


⚙️ Применяемые алгоритмы и паттерны

● Управление состоянием тултипа:
State machine с флагами activeTooltip, isClosing, pendingTarget

● Обработка вложенных блоков:
Рекурсивный обход DOM с ограничением глубины

● Параллельная загрузка данных
$.when() для одновременного запроса первого и последнего сообщения

● Ленивая подгрузка имён
Замена текста на заглушки → асинхронная подстановка имён по post_id

● Позиционирование
Динамический расчёт координат с учётом границ окна

● Изоляция стилей
Использование прямых потомков (>) и !important  для предотвращения конфликтов

● Таймеры и отмена
setTimeout + clearTimeout для управления временем жизни тултипа

Этот код — полноценная система интерактивных превью, сочетающая:

● API-интеграцию (запросы к /api.php),
● Безопасную обработку контента,
● Рекурсивную трансформацию вложенных структур,
● Адаптивное позиционирование,
● Поддержку тёмной темы,
● Оптимизацию UX (очередь, таймеры, плавные анимации).
   
Он не использует внешние библиотеки, кроме jQuery, и полностью автономен внутри замыкания.

Установка на форум

CSS код ставится между тегами <style> </style> в секцию Администрирование - Формы - HTML верх.
JS код ставится между тегами <script> </script> в секцию Администрирование - Формы - HTML низ.


❗Так как скрипт большой и сложный, то, после задания требуемых параметров,  рекомендуется использовать минификацию (сжатие) с помощью сервисов https://minify-js.com/ и https://minify-css.com/ затем сохранить код в файлы, закачать их через админ-панель на ваш сервер и подключать в виде:

в HTML верх

<!-- Превью темы и профиля (Invision Community style) -->
<link rel="stylesheet" href="https://forumstatic.ru/files/001a/f0/7d/91001.css?v=151">
<!-- Превью темы и профиля (Invision Community style) -->

в HTML низ

<!-- Превью темы и профиля (Invision Community style) -->
<script src="https://forumstatic.ru/files/001a/f0/7d/44934.js?v=91"></script>
<!-- Превью темы и профиля (Invision Community style) -->

Этот код используется на моём форуме, а вы вставите свои адреса файлов.
Но для ознакомления с функционалом вы можете использовать вышеприведённый код вставки :)


📜Для редактирования кода рекомендуется использовать бесплатный редактор VS Code.

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

110

Обновление:

В лайв-боксе теперь не показываются темы из разделов  Реклама и  БИОРЕАКТОР.

В Профиле теперь можно отключать раскраску цветных тем. 

Все настройки лайв-бокса в Профиле  - сохраняются в вашем браузере.

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

111

Добавлены настройки для лайв-бокса в Профиле: "Количество отображаемых тем" и "Высота строки" (в пикселях), это позволит точно настроить количество строк и их внешний вид под ваш компьютер и ваше удобство.

В настройки лайв-бокса для мобильного вида также добавлена настройка длины отображаемых ников и количества символов в темах лайв-бокса.

Все настройки лайв-бокса в Профиле  - сохраняются в вашем браузере.

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

112

Надоели кружочки из аватарок рядом с описанием тем, сделал квадратики :)

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

113

Добавлен "Чёрный список" (по многочисленным просьбам).

Вы можете добавлять и убирать пользователя в ЧС на странице его профиля, а на странице своего профиля у вас список пользователей, где их можно также удалять из чёрного списка.

Принцип работы: посты пользователей, попавших в чёрный список, не показываются лично вам.

Особенности:

1) Чёрный список работает в памяти браузера, а не на сервере, поэтому для каждого браузера список может быть свой.

2) Чёрный список не интегрирован с лайв-боксом на главной странице.

3) Полностью поддерживается тёмная тема и мобильный режим.

4) Администратора и модераторов добавить в чёрный список нельзя :) (так задумано).

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

114

В лайв-бокс с темами на главной странице интегрирован "чёрный список" (далее - ЧС).

Введена настройка:

🚫 Игнорировать сообщения от пользователей из чёрного списка
(проверяются только посты на последней странице темы)

Логика работы: если последнее сообщение в любой теме в лайв-боксе  - от юзера из вашего ЧС, то переходить к предыдущему сообщению в этой теме и сравнивать его с ЧС до тех пор, пока не дойдём до сообщения от юзера не от  ЧС, и эту дату  (с временем)  следует считать параметрами последнего поста  в теме.

Если же на последней странице темы вообще нет сообщений не от форумчан из вашего ЧС - тему исключаем из лайв-бокса как неактуальную лично для вас.

Почему фильтрация только на последней странице  - так как считаем, что на последней странице темы сосредоточена актуальная активность.

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

115

Площадка mybb подняла тарифы на отключение рекламы.

https://upforme.ru/uploads/0000/14/1c/38891/299170.jpg

Это выглядит несколько неадекватно.

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

116

30241,2 написал(а):

Это выглядит несколько неадекватно.

Ясен красен! За сумму в сто раз меньше....

Буратино продал какому-то мальчишке за четыре сольдо свою азбуку и купил билет на представление «Девочка с голубыми волосами, или Тридцать три подзатыльника»

... получился бессмертный сюжет!

Подпись автора

"Я не собираюсь расшифровывать, шиза это или такое чувство юмора. По умолчанию - шиза."(С)

+1

117

В общем, я остановился на варианте "показывать рекламу для не авторизованных" + текстовые  рекламные ссылки внизу страницы. А зарегистрированные и вошедшие на форум - баннеров по-прежнему не увидят.
Это получается дешевле и хоть как-то приемлемо.

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

+2

118

1) Улучшено: сейчас "Быстроплюсы и "Сказали спасибо" работают мгновенно и корректно - как при обычной загрузке страницы, так и при "живом" обновлении темы.

2) Наши прекрасные партнёры, то есть площадка mybb опять что-то поломали кое-где в свойствах селекторов, а я что-то где-то исправил и  что-то где-то снова нормально работает.

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

119

Потихоньку готовимся к вариантам будущего: на компьютере  локально (т.е. без выхода в интернет) работает движок современного форума.

https://upforme.ru/uploads/001a/f0/7d/2/170336.webp

Ссылка для полноразмерного просмотра: https://upforme.ru/uploads/001a/f0/7d/2/170336.webp

Подпись автора

Функционал форума Книга жалоб Книга предложений Знак зодиака Как вставить видео на форум Форум"Грибные места" Слайдер для картинок Live-box с темами

0

120

30360,2 написал(а):

на компьютере  локально (т.е. без выхода в интернет) работает движок

и локальные пользователи там же:
- Дедка
- Бабка
- Внучка
- Жучка
... да и хватит уже для форума :)

OpenServer позволяет запускать локально ЛЮБОЙ интернет-проект. Я так и делаю лет 15 уже. а после отладки переношу на production.

Есть нюансы, правда. На реальном хостинге существуют ограничения по кол-ву оперативной памяти, а локально об этом и не задумываешься. Опять же конфиг Апача, что на компе, может отличаться от конфига ДРУГОГО сервера на production. Это просто надо знать и учитывать при переносе.

Подпись автора

"Я не собираюсь расшифровывать, шиза это или такое чувство юмора. По умолчанию - шиза."(С)

0


Вы здесь » kuban-forum.ru - Лучший форум для общения » 🍰Кофейня » Новости форума и обсуждение его работы