(function() {
// 🛠️ === НАСТРОЙКИ СКРИПТА ===
const ALLOWED_FORUM_IDS = []; // 📌 Разрешённые разделы ["1","2"]; (пусто = везде)
const BLOCKED_FORUM_IDS = ["5"]; // 🚫 Заблокированные разделы (приоритет над разрешёнными)
const ENABLE_TOPIC_LINKS = true; // 🔗 Включить автоматическую вставку ссылки "Тема: ..." в посты?
// 📱 Определяем, мобильное ли устройство (ширина экрана ≤ 768px)
const IS_MOBILE = window.innerWidth <= 768;
// 🔍 Вспомогательная функция: извлекает ID раздела (fid) или темы (id) из URL
function getForumOrTopicId() {
const url = window.location.href;
let fidMatch = url.match(/[?&]fid=(\d+)/i); // ID раздела
let vidMatch = url.match(/[?&]id=(\d+)/i); // ID темы
return fidMatch ? fidMatch[1] : (vidMatch ? vidMatch[1] : null);
}
// ✅ Проверяет, разрешён ли текущий раздел/тема для работы скрипта
function isForumAllowed() {
const currentId = getForumOrTopicId();
// ❌ Если ID есть в чёрном списке — запрещаем работу скрипта
if (BLOCKED_FORUM_IDS.length && currentId && BLOCKED_FORUM_IDS.includes(currentId)) {
return false;
}
// ✅ Если белый список пуст — разрешаем везде
if (!ALLOWED_FORUM_IDS.length) return true;
// ✅ Иначе — разрешаем только если ID есть в белом списке
return currentId && ALLOWED_FORUM_IDS.includes(currentId);
}
const forumAllowed = isForumAllowed();
// 🧹 === ОЧИСТКА ЗАГОЛОВКА ВКЛАДКИ И Open Graph ОТ ЦВЕТОВЫХ КОДОВ ===
(function() {
// Применяем только на странице просмотра темы
if (!document.querySelector('.punbb#pun-viewtopic')) return;
let rawSubject = null;
// Пытаемся получить заголовок из глобальной переменной FORUM (если доступна)
if (typeof FORUM !== 'undefined' && FORUM.topic && FORUM.topic.subject) {
rawSubject = FORUM.topic.subject;
} else {
// Иначе — из DOM
const h1 = document.querySelector('.main h1 span');
rawSubject = h1 ? h1.textContent : null;
}
// Если в заголовке есть цветовые коды (;
, очищаем их
const cleanTitle = rawSubject.split(';;').slice(2).join(';;');
// Сохраняем суффикс (например, " — Название форума")
const suffix = document.title.includes(' — ')
? ' — ' + document.title.split(' — ').slice(1).join(' — ')
: '';
document.title = cleanTitle + suffix;
// Также обновляем Open Graph метатег для соцсетей
const ogTitle = document.querySelector('meta[property="og:title"]');
if (ogTitle) ogTitle.setAttribute('content', cleanTitle);
}
})();
// 🎨 === ЗАГРУЗКА БИБЛИОТЕКИ Vanilla Picker (выбор цвета) ===
function loadVanillaPicker() {
return new Promise((resolve, reject) => {
// Если библиотека уже загружена — используем её
if (typeof Picker !== 'undefined') {
resolve(Picker);
return;
}
// Иначе — загружаем скрипт с CDN
const script = document.createElement('script');
script.src = 'https://forumstatic.ru/files/001a/f0/7d/21376.js';
script.onload = () => resolve(window.Picker);
script.onerror = () => reject(new Error('Failed to load Vanilla Picker'));
document.head.appendChild(script);
});
}
// 🎨 === ИНИЦИАЛИЗАЦИЯ ПАНЕЛИ ВЫБОРА ЦВЕТА (только на десктопе!) ===
async function initColorPicker() {
// 📱 На мобильных устройствах — ничего не создаём
if (IS_MOBILE) return;
// 🔒 Проверяем, разрешён ли текущий раздел
if (!forumAllowed) return;
// Находим оригинальное поле ввода темы
const subjectInput = document.querySelector('input[name="req_subject"]');
// Если поле не найдено или интерфейс уже создан — выходим
if (!subjectInput || document.getElementById("color-topic-interface")) return;
// Прячем оригинальное поле, но оставляем его в DOM для отправки формы
subjectInput.style.visibility = "hidden";
subjectInput.style.position = "absolute";
const container = subjectInput.parentNode;
// Убеждаемся, что контейнер позиционирован
if (window.getComputedStyle(container).position === 'static') {
container.style.position = 'relative';
}
// 🔸 === СОЗДАНИЕ ИНТЕРФЕЙСА ВВОДА ТЕМЫ С ЦВЕТАМИ ===
// ⚠️ Обрати внимание: у поля ввода НЕТ атрибута maxlength — он управляется динамически!
const interfaceHTML = `
<div id="color-topic-interface" style="display:flex;flex-direction:column;gap:8px;margin-top:6px;width:100%;font-family:inherit;">
<!-- Поле ввода названия темы + счётчик символов -->
<div style="position: relative; width: 100%;">
<input type="text" id="topic-title-input" placeholder="Название темы"
style="width:100%;padding:7px 50px 7px 10px;border:1px solid #ccc;border-radius:6px;font-size:14px;transition:color .2s ease;box-sizing:border-box;" />
<div id="char-counter" style="position:absolute;right:10px;top:50%;transform:translateY(-50%);font-size:12px;color:#888;pointer-events:none;">0/48</div>
</div>
<!-- Строка с элементами управления цветом и переключателем -->
<div id="color-controls-row" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<!-- Контейнер для всех цветовых элементов (палитра, кнопки) -->
<div id="color-elements" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<!-- Палитра предустановленных цветов -->
<div id="color-palette" style="display:flex;gap:6px;flex-wrap:wrap;">
${['#e74c3c','#e67e22','#f1c40f','#2ecc71','#3498db','#9b59b6','#16a085','#d35400','#c0392b'].map(c =>
`<button type="button" class="palette-color" data-color="${c}" style="width:26px;height:26px;border:1px solid #aaa;border-radius:4px;background:${c};cursor:pointer;"></button>`).join('')}
</div>
<!-- Кнопки выбора цвета текста и фона -->
<div style="display:flex;align-items:center;gap:6px;">
<button type="button" id="open-color-picker-btn" title="Выбрать цвет названия" style="width:34px;height:26px;border:1px solid #aaa;border-radius:6px;background:#f6f6f6;cursor:pointer;">🎨</button>
<div id="current-color-display" style="width:30px;height:26px;border:1px solid #999;border-radius:4px;background:transparent;"></div>
<button type="button" id="open-bg-picker-btn" title="Выбрать цвет фона" style="width:34px;height:26px;border:1px solid #aaa;border-radius:6px;background:#f6f6f6;cursor:pointer;">🖌</button>
<div id="current-bg-display" style="width:30px;height:26px;border:1px solid #999;border-radius:4px;background:transparent;"></div>
</div>
</div>
<!-- Переключатель режима (всегда виден, вне цветных элементов) -->
<div style="display:flex;align-items:center;gap:6px;margin-left:auto;font-size:13px;color:#555;user-select:none;">
<span>Цвет</span>
<label style="display:inline-block;width:40px;height:20px;position:relative;background:#ccc;border-radius:10px;cursor:pointer;">
<input type="checkbox" id="color-mode-toggle" checked style="opacity:0;position:absolute;">
<span id="toggle-thumb" style="position:absolute;top:2px;left:2px;width:16px;height:16px;background:#fff;border-radius:50%;transition:transform 0.2s;"></span>
</label>
</div>
</div>
<!-- Скрытые поля для передачи цветов в форму -->
<input type="hidden" id="selected-color-value" name="topic_color" value="">
<input type="hidden" id="selected-bg-value" name="topic_bg_color" value="">
</div>
`;
container.insertAdjacentHTML("beforeend", interfaceHTML);
// 🔍 Получаем все элементы интерфейса
const titleInput = document.getElementById("topic-title-input");
const colorValueInput = document.getElementById("selected-color-value");
const bgValueInput = document.getElementById("selected-bg-value");
const paletteButtons = document.querySelectorAll(".palette-color");
const openPickerBtn = document.getElementById("open-color-picker-btn");
const openBgBtn = document.getElementById("open-bg-picker-btn");
const currentColorDisplay = document.getElementById("current-color-display");
const currentBgDisplay = document.getElementById("current-bg-display");
const charCounter = document.getElementById("char-counter");
const toggleSwitch = document.getElementById('color-mode-toggle');
const toggleThumb = document.getElementById('toggle-thumb');
const colorElements = document.getElementById('color-elements');
// 🔄 Флаги для отслеживания состояния пикеров
let isTextPickerOpen = false;
let isBgPickerOpen = false;
текст")
function stripColorCode(text) {
if (!text.includes(';;')) return text;
return text.split(';;').slice(2).join(';;');
}
// 📤 Синхронизирует значение с оригинальным полем формы
function updateHiddenSubject() {
const title = titleInput.value.trim();
const color = colorValueInput.value.trim();
const bg = bgValueInput.value.trim();
if (toggleSwitch.checked) {
// В цветном режиме: максимум 48 символов
const finalTitle = title.length > 48 ? title.slice(0, 48) : title;
${finalTitle}` : "";
// Применяем цвета к полю ввода
titleInput.style.color = color || '';
titleInput.style.backgroundColor = bg || '';
} else {
// В простом режиме: только текст
subjectInput.value = title;
titleInput.style.color = '';
titleInput.style.backgroundColor = '';
}
}
// 🔢 Обновляет счётчик символов и визуальную индикацию лимита
function updateCharCounter() {
const len = titleInput.value.length;
// Лимит зависит от режима: 48 (цвет) или 70 (обычный)
const max = toggleSwitch.checked ? 48 : 70;
charCounter.textContent = `${len}/${max}`;
// Меняем цвет счётчика и рамки в зависимости от длины
if (len > max) {
charCounter.style.color = "#e74c3c"; // 🔴 Превышен лимит
titleInput.style.borderColor = "#e74c3c";
} else if (len > (max - 8)) {
charCounter.style.color = "#d35400"; // 🟠 Близко к лимиту
titleInput.style.borderColor = "#d35400";
} else {
charCounter.style.color = "#888"; // ⚪ Норма
titleInput.style.borderColor = "#ccc";
}
}
// 🔄 Инициализация значения из оригинального поля
titleInput.value = stripColorCode(subjectInput.value);
// Устанавливаем начальный лимит в зависимости от состояния переключателя
titleInput.maxLength = toggleSwitch.checked ? 48 : 70;
// 📝 Обработчик ввода текста
titleInput.addEventListener("input", () => {
// Жёсткая обрезка при вводе в цветном режиме (на случай вставки)
if (toggleSwitch.checked && titleInput.value.length > 48) {
titleInput.value = titleInput.value.slice(0, 48);
}
updateHiddenSubject();
updateCharCounter();
});
// 🖥️ Инициализация счётчика
updateCharCounter();
// 🎨 Обработка кликов по палитре цветов
paletteButtons.forEach(btn => {
btn.addEventListener("click", function() {
// Снимаем выделение со всех кнопок
paletteButtons.forEach(b => (b.style.border = "1px solid #aaa"));
// Выделяем текущую
this.style.border = "2px solid #000";
const color = this.dataset.color;
colorValueInput.value = color;
currentColorDisplay.style.backgroundColor = color;
titleInput.style.color = color;
updateHiddenSubject();
});
});
// 🎨 Загружаем библиотеку выбора цвета
const Picker = await loadVanillaPicker();
// === ЦВЕТ ТЕКСТА (расширенный пикер) ===
const textPickerContainer = document.createElement('div');
textPickerContainer.id = 'text-picker-container';
textPickerContainer.style.position = 'absolute';
textPickerContainer.style.top = 'calc(100% + 4px)';
textPickerContainer.style.left = '0';
textPickerContainer.style.zIndex = '1000';
textPickerContainer.style.display = 'none';
textPickerContainer.style.background = '#fff';
textPickerContainer.style.border = '1px solid #ccc';
textPickerContainer.style.borderRadius = '6px';
textPickerContainer.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
openPickerBtn.parentNode.appendChild(textPickerContainer);
const textPicker = new Picker({
parent: textPickerContainer,
popup: false,
color: colorValueInput.value || '#000000',
editorFormat: 'hex',
components: {
interaction: {
ok: true,
cancel: true,
input: true
}
},
onChange: (color) => {
titleInput.style.color = color.rgbaString;
currentColorDisplay.style.backgroundColor = color.rgbaString;
},
onDone: (color) => {
paletteButtons.forEach(b => b.style.border = "1px solid #aaa");
colorValueInput.value = color.hex;
titleInput.style.color = color.rgbaString;
currentColorDisplay.style.backgroundColor = color.rgbaString;
updateHiddenSubject();
textPickerContainer.style.display = 'none';
isTextPickerOpen = false;
},
onCancel: () => {
titleInput.style.color = colorValueInput.value || '';
currentColorDisplay.style.backgroundColor = colorValueInput.value || '';
textPickerContainer.style.display = 'none';
isTextPickerOpen = false;
}
});
openPickerBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (isTextPickerOpen) {
textPickerContainer.style.display = 'none';
isTextPickerOpen = false;
} else {
if (isBgPickerOpen) {
bgPickerContainer.style.display = 'none';
isBgPickerOpen = false;
}
textPickerContainer.style.display = 'block';
isTextPickerOpen = true;
}
});
// === ЦВЕТ ФОНА (расширенный пикер) ===
const bgPickerContainer = document.createElement('div');
bgPickerContainer.id = 'bg-picker-container';
bgPickerContainer.style.position = 'absolute';
bgPickerContainer.style.top = 'calc(100% + 4px)';
bgPickerContainer.style.left = '0';
bgPickerContainer.style.zIndex = '1000';
bgPickerContainer.style.display = 'none';
bgPickerContainer.style.background = '#fff';
bgPickerContainer.style.border = '1px solid #ccc';
bgPickerContainer.style.borderRadius = '6px';
bgPickerContainer.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
openBgBtn.parentNode.appendChild(bgPickerContainer);
const bgPicker = new Picker({
parent: bgPickerContainer,
popup: false,
color: bgValueInput.value || '#ffffff',
editorFormat: 'hex',
components: {
interaction: {
ok: true,
cancel: true,
input: true
}
},
onChange: (color) => {
titleInput.style.backgroundColor = color.hex;
currentBgDisplay.style.backgroundColor = color.rgbaString;
},
onDone: (color) => {
bgValueInput.value = color.hex;
titleInput.style.backgroundColor = color.hex;
currentBgDisplay.style.backgroundColor = color.rgbaString;
updateHiddenSubject();
bgPickerContainer.style.display = 'none';
isBgPickerOpen = false;
},
onCancel: () => {
titleInput.style.backgroundColor = bgValueInput.value || '';
currentBgDisplay.style.backgroundColor = bgValueInput.value || '';
bgPickerContainer.style.display = 'none';
isBgPickerOpen = false;
}
});
openBgBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (isBgPickerOpen) {
bgPickerContainer.style.display = 'none';
isBgPickerOpen = false;
} else {
if (isTextPickerOpen) {
textPickerContainer.style.display = 'none';
isTextPickerOpen = false;
}
bgPickerContainer.style.display = 'block';
isBgPickerOpen = true;
}
});
// 🔄 Восстановление цветов из оригинального поля (если они есть)
const [color, bg, ...rest] = subjectInput.value.split(';;');
const title = rest.join(';;');
colorValueInput.value = color;
bgValueInput.value = bg;
titleInput.value = title;
titleInput.style.color = color;
titleInput.style.backgroundColor = bg;
currentColorDisplay.style.backgroundColor = color;
currentBgDisplay.style.backgroundColor = bg;
// Выделяем соответствующую кнопку в палитре
const match = [...paletteButtons].find(b => b.dataset.color === color);
if (match) match.style.border = "2px solid #000";
}
// 🔘 === УПРАВЛЕНИЕ ПЕРЕКЛЮЧАТЕЛЕМ ===
function updateToggleStyle() {
const isEnabled = toggleSwitch.checked;
if (isEnabled) {
// Включено: ползунок справа, синий фон
toggleThumb.style.transform = 'translateX(20px)';
toggleThumb.parentNode.style.background = '#3498db';
colorElements.style.display = 'flex'; // Показываем панель цветов
} else {
// Выключено: ползунок слева, серый фон
toggleThumb.style.transform = 'translateX(0)';
toggleThumb.parentNode.style.background = '#ccc';
colorElements.style.display = 'none'; // Скрываем панель цветов
}
}
// 🖱️ Обработчик переключения режима
toggleSwitch.addEventListener('change', () => {
const nowEnabled = toggleSwitch.checked;
updateToggleStyle();
if (!nowEnabled) {
// Выключаем цвета: сбрасываем всё
colorValueInput.value = '';
bgValueInput.value = '';
currentColorDisplay.style.backgroundColor = 'transparent';
currentBgDisplay.style.backgroundColor = 'transparent';
paletteButtons.forEach(b => b.style.border = "1px solid #aaa");
titleInput.maxLength = 70; // Увеличиваем лимит до 70
} else {
// Включаем цвета
titleInput.maxLength = 48; // Уменьшаем лимит до 48
if (titleInput.value.length > 48) {
// Автообрезка, если текст слишком длинный
titleInput.value = titleInput.value.slice(0, 48);
}
}
updateHiddenSubject();
updateCharCounter();
});
// 🖥️ Инициализация внешнего вида переключателя
updateToggleStyle();
}
// 🔗 === ДОБАВЛЕНИЕ ССЫЛКИ "ТЕМА: ..." В ПОСТЫ ===
function addTopicLinkToPostHeader() {
if (!ENABLE_TOPIC_LINKS) return;
if (!document.querySelector('.punbb#pun-viewtopic')) return;
const alt = document.querySelector('link[rel="alternate"]');
if (!alt) return;
const match = alt.getAttribute("href").match(/id=(\d+)/);
if (!match) return;
const topicId = match[1];
const topicUrl = "/viewtopic.php?id=" + topicId;
const topicTitle = document.querySelector(".main h1 span")?.textContent?.trim();
if (!topicTitle) return;
const linkHTML = `<li class="TopicLnk" style="font-weight:normal;display:inline-block;color:#7c7cc2;"> Тема: <a href="${topicUrl}">${topicTitle}</a></li>`;
document.querySelectorAll(".post h3>span a.permalink").forEach(link => {
if (link.parentElement.querySelector(".TopicLnk")) return;
const isTopicPost = link.closest(".topicpost") !== null;
const finalHTML = isTopicPost ? linkHTML.replace("Re: ", "") : linkHTML;
link.insertAdjacentHTML("afterend", finalHTML);
});
}
// 🎨 === ПРИМЕНЕНИЕ ЦВЕТОВ КО ВСЕМ ЭЛЕМЕНТАМ НА СТРАНИЦЕ ===
function applyTopicColors() {
// Обрабатываем все элементы, которые могут содержать цветовые коды
document.querySelectorAll(
'a[href*="viewtopic.php"], .TopicLnk, .post-cell.tcr, .crumbs a, .main h1 span'
).forEach(el => {
if (el.dataset.colorProcessed) return; // Уже обработано
let text = el.textContent || "";
const [color, bg, ...rest] = text.split(';;');
const cleanText = rest.join(';;');
// Оборачиваем текст в span с цветами
el.innerHTML = `<span style="color:${color};background-color:${bg};padding:0 2px;border-radius:2px;">${cleanText}</span>`;
el.dataset.colorProcessed = "1";
});
// 🔍 Дополнительно: обход текстовых узлов (для сложных случаев)
const allTextNodes = [];
const walk = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
acceptNode: function(node) {
let currentParent = node.parentElement;
while (currentParent && currentParent !== document.body) {
if (currentParent.dataset.colorProcessed) return NodeFilter.FILTER_REJECT;
currentParent = currentParent.parentElement;
}
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_REJECT;
}
});
let node;
while (node = walk.nextNode()) allTextNodes.push(node);
allTextNodes.forEach(textNode => {
let currentParent = textNode.parentElement;
while (currentParent && currentParent !== document.body) {
if (currentParent.dataset.colorProcessed) return;
currentParent = currentParent.parentElement;
}
const parent = textNode.parentElement;
const text = textNode.textContent;
const [color, bg, ...rest] = text.split(';;');
const cleanText = rest.join(';;');
const coloredSpan = document.createElement('span');
coloredSpan.style.color = color;
coloredSpan.style.backgroundColor = bg;
coloredSpan.style.padding = '0 2px';
coloredSpan.style.borderRadius = '2px';
coloredSpan.textContent = cleanText;
textNode.replaceWith(coloredSpan);
parent.dataset.colorProcessed = '1';
});
}
// 🖥️ Заглушка для будущего расширения (заголовок)
function applyTopicColorsInHeader() {}
// ▶️ === ЗАПУСК СКРИПТА ПРИ ЗАГРУЗКЕ СТРАНИЦЫ ===
document.addEventListener("DOMContentLoaded", () => {
initColorPicker();
addTopicLinkToPostHeader();
applyTopicColors();
applyTopicColorsInHeader();
});
// 👁️ === НАБЛЮДАТЕЛЬ ЗА ИЗМЕНЕНИЯМИ В DOM (для динамических элементов) ===
const liveObserver = new MutationObserver(() => {
applyTopicColors();
applyTopicColorsInHeader();
});
liveObserver.observe(document.body, { childList: true, subtree: true });
})();