С помощью ChatGPT и Claude мне удалось заставить это работать, и теперь решение очень гибкое:
Если вы хотите сделать то же самое, создайте новый компонент и добавьте следующий код во вкладку CSS:
.sidebar-section-link-prefix .emoji.prefix-emoji {
width: 1rem !important;
height: 1rem !important;
}
В моём случае 1rem работает отлично. Настройте значение под свой форум или сообщество.
Затем во вкладку JS добавьте:
import { apiInitializer } from "discourse/lib/api";
import { emojiUrlFor } from "discourse/lib/text";
export default apiInitializer("0.11.1", (api) => {
// Сопоставление названий секций с их ID
const sectionIds = {
"community": 1,
"tiago": 2,
"personal-section": 3,
// Добавьте больше секций здесь
};
// Сопоставление [ID секции, название элемента] с именами эмодзи
const iconReplacements = {
// Секция "Community" (ID: 1)
"1,admin": "wrench",
"1,review": "triangular_flag_on_post",
"1,everything": "books",
"1,my-posts": "writing_hand",
"1,my-messages": "envelope_with_arrow",
// Секция "Tiago" (ID: 2)
"2,Journal": "notebook_with_decorative_cover",
"2,Music": "musical_note",
"2,About": "bust_in_silhouette",
// Секция "Personal" (ID: 3)
"3,Backups": "floppy_disk",
"3,Scheduled": "clock3",
"3,Staff": "lock",
"3,Components": "electric_plug",
};
function replaceIcons() {
Object.entries(sectionIds).forEach(([sectionName, sectionId]) => {
Object.entries(iconReplacements).forEach(([key, emojiName]) => {
const [keyId, itemName] = key.split(',');
// Обрабатываем только если замена относится к текущей секции
if (parseInt(keyId) === sectionId) {
const url = emojiUrlFor(emojiName);
// Пробуем несколько селекторов, чтобы охватить и меню-гамбургер, и фиксированную боковую панель
const selectors = [
// Селектор для фиксированной боковой панели
`div[data-section-name="${sectionName}"] li[data-list-item-name="${itemName}"] .sidebar-section-link-prefix.icon`,
// Селектор для меню-гамбургер (более специфичный)
`.menu-panel div[data-section-name="${sectionName}"] li[data-list-item-name="${itemName}"] .sidebar-section-link-prefix.icon`,
// Универсальный резервный вариант
`[data-section-name="${sectionName}"] [data-list-item-name="${itemName}"] .sidebar-section-link-prefix.icon`
];
for (const selector of selectors) {
const prefix = document.querySelector(selector);
if (prefix && url) {
// Проверяем, не была ли иконка уже заменена, чтобы избежать лишних манипуляций с DOM
if (!prefix.querySelector('.prefix-emoji')) {
prefix.innerHTML = `
<img src="${url}"
title="${emojiName}"
alt="${emojiName}"
class="emoji prefix-emoji">
`;
}
break; // Выходим из цикла, как только нашли и заменили иконку
}
}
}
});
});
}
// Запуск при изменении страницы
api.onPageChange(replaceIcons);
// Также запускаем с задержкой, чтобы перехватить динамически загружаемый контент
api.onPageChange(() => {
setTimeout(replaceIcons, 100);
setTimeout(replaceIcons, 500);
});
// Улучшенный MutationObserver для отслеживания изменений боковой панели
const observer = new MutationObserver((mutations) => {
let shouldReplace = false;
mutations.forEach((mutation) => {
// Отслеживаем добавленные узлы (основная функциональность)
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.classList?.contains('menu-panel') ||
node.querySelector?.('.sidebar-sections') ||
node.classList?.contains('sidebar-sections') ||
node.querySelector?.('[data-section-name]')) {
shouldReplace = true;
}
}
});
// Отслеживаем изменения атрибутов в секциях боковой панели (сворачивание/разворачивание)
if (mutation.type === 'attributes' && mutation.target.nodeType === Node.ELEMENT_NODE) {
const target = mutation.target;
if (target.matches('[data-section-name]') ||
target.closest('[data-section-name]') ||
target.matches('.sidebar-section') ||
target.closest('.sidebar-section')) {
shouldReplace = true;
}
}
// Отслеживаем изменения списка дочерних элементов в секциях боковой панели
if (mutation.type === 'childList' && mutation.target.nodeType === Node.ELEMENT_NODE) {
const target = mutation.target;
if (target.matches('[data-section-name]') ||
target.closest('[data-section-name]') ||
target.querySelector('[data-section-name]')) {
shouldReplace = true;
}
}
});
if (shouldReplace) {
setTimeout(replaceIcons, 50);
}
});
// Начинаем наблюдение с улучшенными опциями
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true, // Отслеживаем изменения атрибутов
attributeFilter: ['class', 'style', 'aria-expanded'], // Распространённые атрибуты, меняющиеся при сворачивании/разворачивании
});
// Дополнительные обработчики событий для типичных взаимодействий с боковой панелью
document.addEventListener('click', (event) => {
// Проверяем, был ли клик по заголовку секции боковой панели или кнопке переключения
if (event.target.closest('.sidebar-section-header') ||
event.target.closest('[data-section-name]') ||
event.target.matches('.sidebar-section-toggle')) {
setTimeout(replaceIcons, 100);
}
});
});
Я реализовал это так, чтобы можно было легко добавлять новые пользовательские секции по имени, присваивая каждой id, а затем создавая сопоставление для каждой секции.
Причина этого в том, что у меня может быть две секции с названием Journal, например, но я хочу использовать для каждой разные эмодзи.
Я почти уверен, что кто-то скажет, что для этого уже есть компонент или плагин
но всё равно было весело перебирать варианты с ChatGPT и Claude, пока всё не заработало так, как я хотел.
Надеюсь, это поможет другим пользователям.
Если вы видите что-то, что можно настроить или улучшить, пожалуйста, поделитесь 