- Восстановлен мягкий цвет пузырей: color-mix(92%) вместо жесткого var(--bs-primary) - Аватары: border-radius 9999px + grid layout для идеальных кругов - Разделители: правильный spacing, font-weight 700, pointer-events, color-mix прозрачность - Добавлены оптимизации скролла: rootMargin 300px, limit 50 сообщений (Telegram-стиль) - Добавлены новые модули: messageLoader, scrollManager, chatUtils - Рефакторинг логики загрузки сообщений для предотвращения дубликатов - Создана документация по архитектуре и тестированию
496 lines
18 KiB
JavaScript
496 lines
18 KiB
JavaScript
/**
|
||
* Аудит CSS стилей на странице чата
|
||
* Проверяет какие стили используются, а какие нет
|
||
*
|
||
* Запуск из консоли браузера:
|
||
*
|
||
* 1. Загрузить скрипт:
|
||
* const script = document.createElement('script');
|
||
* script.src = '/static/js/tests/chatStylesAudit.js';
|
||
* script.type = 'module';
|
||
* document.head.appendChild(script);
|
||
*
|
||
* 2. Запустить аудит:
|
||
* window.ChatStylesAudit.runFullAudit()
|
||
* window.ChatStylesAudit.checkChatDetailStyles()
|
||
* window.ChatStylesAudit.checkMessageStyles()
|
||
* window.ChatStylesAudit.findUnusedStyles()
|
||
*/
|
||
|
||
class ChatStylesAudit {
|
||
constructor() {
|
||
this.results = {
|
||
total: 0,
|
||
used: 0,
|
||
unused: 0,
|
||
details: []
|
||
};
|
||
|
||
// CSS файлы для проверки
|
||
this.cssFiles = [
|
||
'/static/css/components/chat-detail.css',
|
||
'/static/css/message-reactions.css',
|
||
'/static/css/message-context-menu.css',
|
||
'/static/css/message-selection.css',
|
||
'/static/css/components/chat-polls.css'
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Получает все CSS правила из stylesheet
|
||
*/
|
||
getCSSRules(stylesheet) {
|
||
try {
|
||
return Array.from(stylesheet.cssRules || stylesheet.rules || []);
|
||
} catch (e) {
|
||
console.warn('Не удалось получить правила из stylesheet:', e);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Извлекает селекторы из CSS правила
|
||
*/
|
||
extractSelectors(rule) {
|
||
const selectors = [];
|
||
|
||
if (rule.selectorText) {
|
||
// Разбиваем составные селекторы
|
||
const parts = rule.selectorText.split(',').map(s => s.trim());
|
||
selectors.push(...parts);
|
||
}
|
||
|
||
// Рекурсивно обрабатываем медиа-запросы
|
||
if (rule.cssRules) {
|
||
for (const subRule of this.getCSSRules(rule)) {
|
||
selectors.push(...this.extractSelectors(subRule));
|
||
}
|
||
}
|
||
|
||
return selectors;
|
||
}
|
||
|
||
/**
|
||
* Проверяет используется ли селектор в DOM
|
||
*/
|
||
isSelectorUsed(selector) {
|
||
try {
|
||
// Пропускаем псевдо-элементы и псевдо-классы для точной проверки
|
||
const cleanSelector = selector
|
||
.replace(/::(before|after|placeholder|selection)/g, '')
|
||
.replace(/:(hover|focus|active|visited|link|first-child|last-child|nth-child\([^)]+\)|not\([^)]+\))/g, '')
|
||
.trim();
|
||
|
||
if (!cleanSelector || cleanSelector === '') {
|
||
return { used: false, reason: 'empty_after_cleanup' };
|
||
}
|
||
|
||
const elements = document.querySelectorAll(cleanSelector);
|
||
return {
|
||
used: elements.length > 0,
|
||
count: elements.length,
|
||
selector: cleanSelector
|
||
};
|
||
} catch (e) {
|
||
return { used: false, reason: 'invalid_selector', error: e.message };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Сканирует все стили связанные с чатом
|
||
*/
|
||
async scanChatStyles() {
|
||
console.log('🔍 Сканирование CSS стилей чата...\n');
|
||
|
||
const allSelectors = new Map(); // selector -> { used, files, count }
|
||
|
||
// Проходим по всем загруженным stylesheets
|
||
for (const stylesheet of document.styleSheets) {
|
||
try {
|
||
const href = stylesheet.href || 'inline';
|
||
|
||
// Проверяем только файлы связанные с чатом
|
||
const isChatRelated = this.cssFiles.some(file => href.includes(file)) ||
|
||
href.includes('chat') ||
|
||
href.includes('message');
|
||
|
||
if (!isChatRelated && stylesheet.href) continue;
|
||
|
||
const rules = this.getCSSRules(stylesheet);
|
||
|
||
for (const rule of rules) {
|
||
const selectors = this.extractSelectors(rule);
|
||
|
||
for (const selector of selectors) {
|
||
if (!allSelectors.has(selector)) {
|
||
const usage = this.isSelectorUsed(selector);
|
||
allSelectors.set(selector, {
|
||
original: selector,
|
||
used: usage.used,
|
||
count: usage.count || 0,
|
||
files: [href],
|
||
reason: usage.reason
|
||
});
|
||
} else {
|
||
const existing = allSelectors.get(selector);
|
||
if (!existing.files.includes(href)) {
|
||
existing.files.push(href);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn('Ошибка при обработке stylesheet:', e);
|
||
}
|
||
}
|
||
|
||
return allSelectors;
|
||
}
|
||
|
||
/**
|
||
* Проверяет конкретный CSS файл
|
||
*/
|
||
async checkSpecificFile(filePath) {
|
||
console.log(`📄 Проверка файла: ${filePath}\n`);
|
||
|
||
const results = {
|
||
file: filePath,
|
||
total: 0,
|
||
used: 0,
|
||
unused: 0,
|
||
selectors: []
|
||
};
|
||
|
||
for (const stylesheet of document.styleSheets) {
|
||
if (stylesheet.href && stylesheet.href.includes(filePath)) {
|
||
const rules = this.getCSSRules(stylesheet);
|
||
|
||
for (const rule of rules) {
|
||
const selectors = this.extractSelectors(rule);
|
||
|
||
for (const selector of selectors) {
|
||
const usage = this.isSelectorUsed(selector);
|
||
results.total++;
|
||
|
||
if (usage.used) {
|
||
results.used++;
|
||
} else {
|
||
results.unused++;
|
||
}
|
||
|
||
results.selectors.push({
|
||
selector,
|
||
used: usage.used,
|
||
count: usage.count || 0,
|
||
reason: usage.reason
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* Полный аудит всех стилей
|
||
*/
|
||
async runFullAudit() {
|
||
console.log('\n' + '='.repeat(80));
|
||
console.log('🎨 ПОЛНЫЙ АУДИТ CSS СТИЛЕЙ ЧАТА');
|
||
console.log('='.repeat(80) + '\n');
|
||
|
||
const allSelectors = await this.scanChatStyles();
|
||
|
||
const used = [];
|
||
const unused = [];
|
||
|
||
for (const [selector, data] of allSelectors) {
|
||
if (data.used) {
|
||
used.push({ selector, ...data });
|
||
} else {
|
||
unused.push({ selector, ...data });
|
||
}
|
||
}
|
||
|
||
// Сортируем по количеству использований
|
||
used.sort((a, b) => b.count - a.count);
|
||
unused.sort((a, b) => a.selector.localeCompare(b.selector));
|
||
|
||
console.log('📊 СТАТИСТИКА:');
|
||
console.log('─'.repeat(80));
|
||
console.log(`Всего селекторов: ${allSelectors.size}`);
|
||
console.log(`✅ Используется: ${used.length} (${(used.length / allSelectors.size * 100).toFixed(1)}%)`);
|
||
console.log(`❌ Не используется: ${unused.length} (${(unused.length / allSelectors.size * 100).toFixed(1)}%)`);
|
||
console.log('─'.repeat(80) + '\n');
|
||
|
||
// ТОП-10 самых используемых стилей
|
||
console.log('🏆 ТОП-10 САМЫХ ИСПОЛЬЗУЕМЫХ СТИЛЕЙ:');
|
||
console.log('─'.repeat(80));
|
||
used.slice(0, 10).forEach((item, i) => {
|
||
console.log(`${i + 1}. ${item.selector}`);
|
||
console.log(` 📍 Использований: ${item.count}`);
|
||
console.log(` 📁 Файлы: ${item.files.join(', ')}\n`);
|
||
});
|
||
|
||
// Неиспользуемые стили
|
||
if (unused.length > 0) {
|
||
console.log('\n❌ НЕИСПОЛЬЗУЕМЫЕ СТИЛИ:');
|
||
console.log('─'.repeat(80));
|
||
|
||
// Группируем по файлам
|
||
const byFile = {};
|
||
unused.forEach(item => {
|
||
item.files.forEach(file => {
|
||
if (!byFile[file]) byFile[file] = [];
|
||
byFile[file].push(item.selector);
|
||
});
|
||
});
|
||
|
||
for (const [file, selectors] of Object.entries(byFile)) {
|
||
const fileName = file.split('/').pop();
|
||
console.log(`\n📄 ${fileName} (${selectors.length} неиспользуемых):`);
|
||
selectors.slice(0, 20).forEach(sel => {
|
||
console.log(` • ${sel}`);
|
||
});
|
||
if (selectors.length > 20) {
|
||
console.log(` ... и ещё ${selectors.length - 20}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log('\n' + '='.repeat(80));
|
||
console.log('✅ АУДИТ ЗАВЕРШЁН');
|
||
console.log('='.repeat(80) + '\n');
|
||
|
||
// Сохраняем результаты
|
||
this.lastAudit = {
|
||
timestamp: new Date().toISOString(),
|
||
total: allSelectors.size,
|
||
used: used.length,
|
||
unused: unused.length,
|
||
usedSelectors: used,
|
||
unusedSelectors: unused
|
||
};
|
||
|
||
return this.lastAudit;
|
||
}
|
||
|
||
/**
|
||
* Проверяет стили chat-detail.css
|
||
*/
|
||
async checkChatDetailStyles() {
|
||
console.log('\n📋 АУДИТ: chat-detail.css\n');
|
||
const result = await this.checkSpecificFile('chat-detail.css');
|
||
this.printFileResults(result);
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Проверяет стили сообщений
|
||
*/
|
||
async checkMessageStyles() {
|
||
console.log('\n📋 АУДИТ: Стили сообщений\n');
|
||
|
||
const files = [
|
||
'message-reactions.css',
|
||
'message-context-menu.css',
|
||
'message-selection.css'
|
||
];
|
||
|
||
const results = [];
|
||
for (const file of files) {
|
||
const result = await this.checkSpecificFile(file);
|
||
results.push(result);
|
||
}
|
||
|
||
// Общая статистика
|
||
const total = results.reduce((sum, r) => sum + r.total, 0);
|
||
const used = results.reduce((sum, r) => sum + r.used, 0);
|
||
const unused = results.reduce((sum, r) => sum + r.unused, 0);
|
||
|
||
console.log('\n' + '='.repeat(60));
|
||
console.log('📊 ОБЩАЯ СТАТИСТИКА ПО СТИЛЯМ СООБЩЕНИЙ:');
|
||
console.log('='.repeat(60));
|
||
console.log(`Всего: ${total}`);
|
||
console.log(`✅ Используется: ${used} (${(used / total * 100).toFixed(1)}%)`);
|
||
console.log(`❌ Не используется: ${unused} (${(unused / total * 100).toFixed(1)}%)`);
|
||
console.log('='.repeat(60) + '\n');
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* Находит дубликаты стилей
|
||
*/
|
||
findDuplicateStyles() {
|
||
console.log('\n🔎 ПОИСК ДУБЛИРУЮЩИХСЯ СТИЛЕЙ...\n');
|
||
|
||
const styleMap = new Map(); // CSS text -> selectors[]
|
||
|
||
for (const stylesheet of document.styleSheets) {
|
||
try {
|
||
const rules = this.getCSSRules(stylesheet);
|
||
|
||
for (const rule of rules) {
|
||
if (rule.style && rule.selectorText) {
|
||
const cssText = rule.style.cssText;
|
||
if (!styleMap.has(cssText)) {
|
||
styleMap.set(cssText, []);
|
||
}
|
||
styleMap.get(cssText).push(rule.selectorText);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// Skip
|
||
}
|
||
}
|
||
|
||
const duplicates = [];
|
||
for (const [cssText, selectors] of styleMap) {
|
||
if (selectors.length > 1) {
|
||
duplicates.push({ cssText, selectors, count: selectors.length });
|
||
}
|
||
}
|
||
|
||
duplicates.sort((a, b) => b.count - a.count);
|
||
|
||
console.log(`Найдено ${duplicates.length} групп дублирующихся стилей:\n`);
|
||
|
||
duplicates.slice(0, 10).forEach((dup, i) => {
|
||
console.log(`${i + 1}. ${dup.count} дубликатов:`);
|
||
console.log(` Стили: ${dup.cssText.substring(0, 100)}${dup.cssText.length > 100 ? '...' : ''}`);
|
||
console.log(` Селекторы:`);
|
||
dup.selectors.forEach(sel => console.log(` • ${sel}`));
|
||
console.log('');
|
||
});
|
||
|
||
return duplicates;
|
||
}
|
||
|
||
/**
|
||
* Выводит результаты для файла
|
||
*/
|
||
printFileResults(result) {
|
||
console.log('─'.repeat(60));
|
||
console.log(`📄 Файл: ${result.file}`);
|
||
console.log('─'.repeat(60));
|
||
console.log(`Всего селекторов: ${result.total}`);
|
||
console.log(`✅ Используется: ${result.used} (${(result.used / result.total * 100).toFixed(1)}%)`);
|
||
console.log(`❌ Не используется: ${result.unused} (${(result.unused / result.total * 100).toFixed(1)}%)`);
|
||
console.log('─'.repeat(60));
|
||
|
||
if (result.unused > 0) {
|
||
const unusedList = result.selectors.filter(s => !s.used);
|
||
console.log(`\n❌ Неиспользуемые селекторы (${unusedList.length}):`);
|
||
unusedList.slice(0, 20).forEach(s => {
|
||
console.log(` • ${s.selector}`);
|
||
});
|
||
if (unusedList.length > 20) {
|
||
console.log(` ... и ещё ${unusedList.length - 20}`);
|
||
}
|
||
}
|
||
|
||
console.log('\n');
|
||
}
|
||
|
||
/**
|
||
* Анализирует специфичность селекторов
|
||
*/
|
||
analyzeSpecificity() {
|
||
console.log('\n⚖️ АНАЛИЗ СПЕЦИФИЧНОСТИ СЕЛЕКТОРОВ...\n');
|
||
|
||
const specificityMap = new Map();
|
||
|
||
for (const stylesheet of document.styleSheets) {
|
||
try {
|
||
const rules = this.getCSSRules(stylesheet);
|
||
|
||
for (const rule of rules) {
|
||
if (rule.selectorText) {
|
||
const selectors = rule.selectorText.split(',').map(s => s.trim());
|
||
|
||
for (const selector of selectors) {
|
||
const spec = this.calculateSpecificity(selector);
|
||
const key = spec.join('-');
|
||
|
||
if (!specificityMap.has(key)) {
|
||
specificityMap.set(key, []);
|
||
}
|
||
specificityMap.get(key).push(selector);
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// Skip
|
||
}
|
||
}
|
||
|
||
const entries = Array.from(specificityMap.entries())
|
||
.map(([key, selectors]) => ({
|
||
specificity: key,
|
||
count: selectors.length,
|
||
examples: selectors.slice(0, 3)
|
||
}))
|
||
.sort((a, b) => b.count - a.count);
|
||
|
||
console.log('📊 Распределение по специфичности:\n');
|
||
entries.slice(0, 15).forEach((entry, i) => {
|
||
console.log(`${i + 1}. [${entry.specificity}] - ${entry.count} селекторов`);
|
||
entry.examples.forEach(ex => console.log(` ${ex}`));
|
||
console.log('');
|
||
});
|
||
|
||
return entries;
|
||
}
|
||
|
||
/**
|
||
* Вычисляет специфичность селектора
|
||
*/
|
||
calculateSpecificity(selector) {
|
||
let ids = 0;
|
||
let classes = 0;
|
||
let elements = 0;
|
||
|
||
// Подсчет ID
|
||
ids = (selector.match(/#/g) || []).length;
|
||
|
||
// Подсчет классов, атрибутов, псевдоклассов
|
||
classes = (selector.match(/\./g) || []).length;
|
||
classes += (selector.match(/\[/g) || []).length;
|
||
classes += (selector.match(/:/g) || []).length;
|
||
|
||
// Подсчет элементов и псевдоэлементов
|
||
elements = (selector.match(/\b[a-z]+\b/g) || []).length;
|
||
elements += (selector.match(/::/g) || []).length;
|
||
|
||
return [ids, classes, elements];
|
||
}
|
||
|
||
/**
|
||
* Экспортирует результаты в JSON
|
||
*/
|
||
exportResults() {
|
||
if (!this.lastAudit) {
|
||
console.error('❌ Сначала запустите runFullAudit()');
|
||
return;
|
||
}
|
||
|
||
const json = JSON.stringify(this.lastAudit, null, 2);
|
||
const blob = new Blob([json], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `chat-styles-audit-${Date.now()}.json`;
|
||
a.click();
|
||
|
||
console.log('✅ Результаты экспортированы в JSON');
|
||
}
|
||
}
|
||
|
||
// Экспортируем в глобальную область
|
||
window.ChatStylesAudit = new ChatStylesAudit();
|
||
|
||
export default ChatStylesAudit;
|