/
Rstycl
/
PixsoChekerTypo
Обзор
Документация
Войти
/
Rstycl
/
PixsoChekerTypo
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
ui.html
1 534 строки
48 KB
Павленко Александр Сергеевич
feat: объединение правил сокращений единиц измерения в одну карточку
06 авг 2026, 13:00
06 авг 2026, 13:00
8a73b47
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Editorial Checker</title> <style> /* Внешних запросов нет: Inter берётся из системы, иначе — системный шрифт */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } .visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 12px; color: #1a1a1a; background: #f5f5f5; height: 100vh; display: flex; flex-direction: column; overflow: hidden; } /* Tab bar */ .tab-bar-wrap { position: relative; background: #fff; flex-shrink: 0; } .tab-bar { display: flex; gap: 24px; padding: 16px 16px 0 16px; position: relative; z-index: 1; } .tab-bar-rule { position: absolute; left: 16px; right: 16px; bottom: 0; height: 1px; background: rgba(6, 10, 12, 0.06); } .tab { display: inline-flex; flex-direction: column; align-items: center; background: none; border: none; padding: 0; cursor: pointer; } .tab-label { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; font-weight: 500; line-height: 1; padding: 5px 0; color: rgba(19, 24, 27, 0.56); white-space: nowrap; transition: color 0.15s; } .tab.active .tab-label { color: rgba(6, 10, 12, 0.96); } .tab:hover:not(.active) .tab-label { color: rgba(19, 24, 27, 0.8); } .tab-underline { height: 2px; width: 100%; margin-top: 5px; border-radius: 1px; background: transparent; } .tab.active .tab-underline { background: rgba(6, 10, 12, 0.96); } .tab-badge { display: inline-block; background: rgba(6, 10, 12, 0.06); color: rgba(19, 24, 27, 0.56); font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 10px; font-weight: 500; padding: 1px 5px; border-radius: 8px; margin-left: 4px; vertical-align: middle; } .tab.active .tab-badge { background: rgba(6, 10, 12, 0.1); color: rgba(6, 10, 12, 0.7); } /* Panels */ .panel { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .panel.hidden { display: none; } /* Check panel */ .toolbar { padding: 24px 16px; background: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; flex: 1; } .btn-scan { height: 40px; padding: 0 24px; background: #2969f3; color: #fff; border: none; border-radius: 4px; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; font-weight: 500; cursor: pointer; transition: background 0.15s; } .btn-scan:hover { background: #1f57d1; } .btn-scan:disabled { background: #9db8f7; cursor: not-allowed; } .toolbar-hint { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; font-weight: 400; color: rgba(19, 24, 27, 0.56); text-align: center; } .toolbar-hint.toolbar-error { color: #d33a2c; } .state-msg { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 6px; color: #999; font-size: 12px; text-align: center; padding: 24px; } .state-msg .icon { font-size: 28px; } .state-msg .title { font-size: 13px; font-weight: 600; color: #555; } /* Settings panel */ .settings-toolbar { height: 34px; box-sizing: border-box; padding: 10px 16px; background: #fff; display: flex; align-items: center; justify-content: flex-start; flex-shrink: 0; } .btn-toggle-all { background: none; border: none; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 12px; font-weight: 500; color: #1549ab; cursor: pointer; } .btn-toggle-all:hover { color: #2969e3; } .settings-list { flex: 1; overflow-y: auto; scrollbar-gutter: stable; background: #fff; padding: 0 16px 16px; display: flex; flex-direction: column; gap: 8px; } .settings-cat { flex-shrink: 0; margin-top: 8px; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 11px; font-weight: 500; color: rgba(19, 24, 27, 0.56); } .settings-cat:first-child { margin-top: 0; } .settings-rule { flex-shrink: 0; height: 32px; box-sizing: border-box; display: flex; align-items: center; padding: 8px 16px; background: #f7f9fb; border-radius: 8px; cursor: default; transition: background 0.12s; } .settings-rule:hover { background: #f2f5f8; } .rule-left { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1; } .rule-name { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 12px; font-weight: 400; color: #060A0C; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rule-info-icon { width: 16px; height: 16px; flex-shrink: 0; margin-left: auto; color: rgba(6, 10, 12, 0.35); cursor: pointer; opacity: 0; pointer-events: none; transition: opacity 0.12s, color 0.12s; } .settings-rule:hover .rule-info-icon { opacity: 1; pointer-events: auto; } .rule-info-icon:hover { color: #2969e3; } #rule-tooltip { position: fixed; display: none; max-width: 220px; background: #000; color: #fff; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 11px; font-weight: 400; line-height: 1.4; padding: 6px 8px; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); z-index: 1000; pointer-events: none; } .rule-switch { position: relative; flex-shrink: 0; width: 32px; height: 16px; } .rule-switch input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); } .switch-track { position: absolute; inset: 0; background: rgba(42, 114, 168, 0.2); border-radius: 999px; cursor: pointer; transition: background 0.18s; } .switch-thumb { position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; background: #fff; border-radius: 50%; transition: transform 0.18s; } .rule-switch input:checked + .switch-track { background: #28d247; } .rule-switch input:checked + .switch-track .switch-thumb { transform: translateX(16px); } .rule-switch:hover input:checked + .switch-track { background: #24b23e; } ::-webkit-scrollbar { width: 4px; } ::-webkit-scrollbar-thumb { background: #ddd; border-radius: 2px; } .spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid #ccc; border-top-color: #555; border-radius: 50%; animation: spin 0.7s linear infinite; vertical-align: middle; margin-right: 4px; } @keyframes spin { to { transform: rotate(360deg); } } /* Страница результатов проверки */ .page-results { flex: 1; background: #fff; display: flex; flex-direction: column; overflow: hidden; } .page-results.hidden { display: none; } .page-results-header { display: flex; align-items: center; gap: 8px; padding: 16px 12px; flex-shrink: 0; } .btn-back { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; padding: 0; background: none; border: none; color: rgba(6, 10, 12, 0.96); cursor: pointer; flex-shrink: 0; } .page-results-title { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 16px; font-weight: 500; color: rgba(6, 10, 12, 0.96); } .page-results-body { flex: 1; min-height: 0; overflow-y: auto; scrollbar-gutter: stable; padding: 0 16px 16px; display: flex; flex-direction: column; gap: 12px; } .page-results-body.hidden { display: none; } /* Карточка ошибки */ .result-card { flex-shrink: 0; border-radius: 8px; overflow: hidden; border: 1px solid rgba(6, 10, 12, 0.08); } .card-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: rgba(9, 43, 60, 0.06); border-radius: 8px 8px 0 0; height: 36px; box-sizing: border-box; } .card-fix-all-btn { opacity: 0; pointer-events: none; display: inline-flex; align-items: center; justify-content: center; gap: 5px; flex-shrink: 0; margin-left: auto; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 12px; font-weight: 500; color: #fff; background: #2969f3; border: none; border-radius: 6px; padding: 4px 9px; cursor: pointer; transition: opacity 0.12s; } .card-header:hover .card-fix-all-btn { opacity: 1; pointer-events: auto; } .card-fix-all-btn svg { width: 13px; height: 13px; } .card-fix-all-btn:hover { background: #1f57d1; } .card-title { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; font-weight: 400; color: rgba(6, 10, 12, 0.96); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .card-counter { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; font-weight: 400; color: rgba(19, 24, 27, 0.56); flex-shrink: 0; margin-left: 8px; } .card-collapse-btn { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 16px; height: 16px; margin-left: 4px; padding: 0; background: none; border: none; color: rgba(6, 10, 12, 0.56); cursor: pointer; transition: transform 0.12s, color 0.12s; } .card-collapse-btn svg { width: 14px; height: 14px; } .card-collapse-btn:hover { color: rgba(6, 10, 12, 0.9); } .card-collapse-btn { transform: rotate(90deg); } .card-header.is-collapsed .card-collapse-btn { transform: rotate(0deg); } .card-body { display: flex; flex-direction: column; } .card-body.hidden { display: none; } .card-row { position: relative; display: flex; align-items: center; padding: 10px 12px; background: #fff; cursor: pointer; transition: background 0.12s; } .card-row:hover { background: rgba(42, 114, 248, 0.12); } .row-fix-btn { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); z-index: 5; display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; background: #2969f3; color: #fff; border: none; border-radius: 6px; cursor: pointer; box-shadow: 0 2px 8px rgba(9, 43, 60, 0.28); opacity: 0; pointer-events: none; transition: opacity 0.12s; } .row-fix-btn svg { width: 14px; height: 14px; } .card-row:hover .row-fix-btn { opacity: 1; pointer-events: auto; } .row-fix-btn:hover { background: #1f57d1; } .card-row-group-wrap { display: flex; flex-direction: column; } .row-chevron { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 16px; height: 16px; margin-left: 8px; color: rgba(6, 10, 12, 0.56); transition: transform 0.12s; } .row-chevron svg { width: 14px; height: 14px; } .card-row-group.is-expanded .row-chevron { transform: rotate(90deg); } .card-row-group.is-expanded { background: #e8eef2; } .card-row-group.is-expanded:hover { background: #e8eef2; } .diff-before.row-error-text { flex: 0 1 auto; max-width: 200px; min-width: 0; line-height: 1.1; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; font-weight: 400; } .row-count-badge { flex-shrink: 0; margin-left: 8px; display: inline-flex; align-items: center; height: 14px; padding: 0 6px; border-radius: 16px; background: #e8eef2; color: rgba(19, 24, 27, 0.56); font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 12px; font-weight: 400; } .card-row-group.is-expanded .row-count-badge { background: #d5dfe6; } .group-instances { background: #f2f5f8; } .card-row-instance { padding-left: 32px; } .row-diff { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; font-weight: 400; } .diff-before { color: rgba(6, 10, 12, 0.96); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .diff-hit { background: #fce8e6; color: #d33a2c; border-radius: 3px; padding: 0 2px; } .diff-arrow { color: #1e9e5a; flex-shrink: 0; } .diff-after { color: rgba(6, 10, 12, 0.96); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .diff-after-hit { color: #1e9e5a; } .diff-after.is-fix-hint { color: rgba(19, 24, 27, 0.56); } /* Пустое состояние результатов */ .empty-state { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 0 24px; } .empty-state.hidden { display: none; } .empty-state-title { font-size: 16px; font-weight: 400; color: #060A0CF5; margin-bottom: 8px; } .empty-state-subtitle { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; font-weight: 400; line-height: 16px; letter-spacing: 0px; color: rgba(19, 24, 27, 0.56); margin-bottom: 16px; text-align: left; } .empty-state .btn-scan { width: 100%; max-width: 200px; } </style> </head> <body> <div class="tab-bar-wrap"> <div class="tab-bar"> <button class="tab active" id="tab-check"> <span class="tab-label">Проверка</span> <span class="tab-underline"></span> </button> <button class="tab" id="tab-settings"> <span class="tab-label">Настройки<span class="tab-badge" id="settings-tab-badge" style="display:none"></span></span> <span class="tab-underline"></span> </button> </div> <div class="tab-bar-rule"></div> </div> <!-- Панель проверки --> <div class="panel" id="panel-check"> <div class="toolbar" id="toolbar"> <button class="btn-scan" id="btn-scan">Сканировать</button> <span class="toolbar-hint" id="scope-hint">Выберите фреймы или отсканируем всю страницу</span> <span class="toolbar-hint toolbar-error" id="scan-error" hidden></span> </div> </div> <div class="visually-hidden" id="sr-status" role="status" aria-live="polite"></div> <div id="rule-tooltip"></div> <!-- Панель настроек --> <div class="panel hidden" id="panel-settings"> <div class="settings-toolbar"> <button class="btn-toggle-all" id="btn-toggle-all">Выключить все</button> </div> <div class="settings-list" id="settings-list"> <div class="state-msg"> <div class="icon">⏳</div> <div class="title">Загрузка…</div> </div> </div> </div> <!-- Страница результатов проверки --> <div class="page-results hidden" id="page-results"> <div class="page-results-header"> <button class="btn-back" id="btn-back-results" aria-label="Назад" title="Назад"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M15 6l-6 6l6 6" /> </svg> </button> <span class="page-results-title">Результаты проверки</span> </div> <div class="page-results-body" id="page-results-body"></div> <div class="empty-state hidden" id="page-results-empty-state"> <div class="empty-state-title">Нарушений не найдено</div> <div class="empty-state-subtitle" id="empty-state-layers-count"></div> <button class="btn-scan" id="btn-scan-again">Сканировать заново</button> </div> </div> <script> var allRules = []; var enabledIds = []; var allViolations = []; var totalNodesChecked = 0; var expandedGroups = {}; var collapsedRules = {}; // свёрнутые карточки правил (ключ — ruleId, запоминаем между сканами) // ─── Init ───────────────────────────────────────────────────────────────── // Плагин теряет фокус окна, но браузер иногда оставляет «залипший» :hover/:focus // на элементе под курсором — из-за этого кнопки автофикса остаются видимыми. window.addEventListener('blur', function() { if (document.activeElement && document.activeElement !== document.body) { document.activeElement.blur(); } }); parent.postMessage({ pluginMessage: { type: 'INIT_REQUEST' } }, '*'); // Обработчики входящих сообщений от code.js, по типу сообщения. var MESSAGE_HANDLERS = { INIT: function(msg) { allRules = msg.rules || []; enabledIds = (msg.enabledIds || []).slice(); buildSettingsPanel(); updateSettingsTabBadge(); }, COLLAPSED_RULES: function(msg) { // Запоминаем свёрнутые правила. Не вызываем renderResults: при старте // плагина панель ещё на стартовом экране; состояние применится при // рендере результата сканирования (VIOLATIONS / renderResultCards). collapsedRules = {}; (msg.collapsed || []).forEach(function(id) { collapsedRules[id] = true; }); }, SCOPE: function(msg) { var hint = document.getElementById('scope-hint'); if (hint) { hint.textContent = msg.count > 0 ? 'Выбрано: ' + msg.count + ' ' + plural(msg.count, 'фрейм', 'фрейма', 'фреймов') : 'Выберите фреймы или отсканируем всю страницу'; } }, VIOLATIONS: function(msg) { clearTimeout(scanTimer); hideScanError(); setScanning(false); allViolations = msg.items || []; totalNodesChecked = msg.totalNodes || 0; expandedGroups = {}; // не тащим развороты из прошлого скана renderResults(); }, SCAN_ERROR: function(msg) { clearTimeout(scanTimer); onScanFailed('Не удалось выполнить сканирование.'); }, AUTOFIX_DONE: function(msg) { removeFixedInstances(msg.ruleId, [{ match: msg.match, targets: msg.targets || [] }]); rerenderAfterFix(); }, AUTOFIX_ALL_DONE: function(msg) { removeFixedInstances(msg.ruleId, msg.items || []); rerenderAfterFix(); } }; function rerenderAfterFix() { if (!allViolations.length) renderResults(); // всё исправлено → пустое состояние else renderResultCards(allViolations); } // Убирает исправленные вхождения из сгруппированных нарушений — точечно, // по паре (nodeId, matchIndex); группа целиком пропадает из списка, когда // в ней не осталось вхождений. function removeFixedInstances(ruleId, fixedItems) { allViolations = allViolations.map(function(v) { if (v.ruleId !== ruleId) return v; var fixedKeys = {}; fixedItems.forEach(function(item) { if (item.match !== v.match) return; (item.targets || []).forEach(function(t) { fixedKeys[t.nodeId + '|' + t.matchIndex] = true; }); }); var instances = (v.instances || []).filter(function(inst) { return !fixedKeys[inst.nodeId + '|' + inst.matchIndex]; }); if (instances.length === (v.instances || []).length) return v; return Object.assign({}, v, { instances: instances, count: instances.length }); }).filter(function(v) { return v.count > 0; }); } // Pixso uses window.addEventListener('message', ...) instead of window.onmessage window.addEventListener('message', function(event) { var msg = event.data && event.data.pluginMessage; if (!msg) return; var handler = MESSAGE_HANDLERS[msg.type]; if (handler) handler(msg); }); // ─── Results page ───────────────────────────────────────────────────────── function showCheckPanel() { document.getElementById('page-results').classList.add('hidden'); document.getElementById('page-results-body').classList.remove('hidden'); document.getElementById('page-results-empty-state').classList.add('hidden'); var panelCheck = document.getElementById('panel-check'); if (panelCheck) panelCheck.classList.remove('hidden'); var tabBarWrap = document.querySelector('.tab-bar-wrap'); if (tabBarWrap) tabBarWrap.style.display = ''; var toolbar = document.getElementById('toolbar'); if (toolbar) toolbar.style.display = 'flex'; switchTab('check'); } document.getElementById('btn-back-results').onclick = showCheckPanel; document.getElementById('btn-scan-again').onclick = startScan; // ─── Tabs ───────────────────────────────────────────────────────────────── document.getElementById('tab-check').onclick = function() { switchTab('check'); }; document.getElementById('tab-settings').onclick = function() { switchTab('settings'); }; function switchTab(name) { document.getElementById('tab-check').classList.toggle('active', name === 'check'); document.getElementById('tab-settings').classList.toggle('active', name === 'settings'); document.getElementById('panel-check').classList.toggle('hidden', name !== 'check'); document.getElementById('panel-settings').classList.toggle('hidden', name !== 'settings'); document.getElementById('page-results').classList.add('hidden'); document.getElementById('page-results-body').classList.remove('hidden'); document.getElementById('page-results-empty-state').classList.add('hidden'); } // ─── Scan ───────────────────────────────────────────────────────────────── var scanTimer = null; var SCAN_TIMEOUT_MS = 15000; function startScan() { if (!enabledIds.length) return; hideScanError(); setScanning(true); clearTimeout(scanTimer); scanTimer = setTimeout(function() { onScanFailed('Плагин не ответил на запрос сканирования.'); }, SCAN_TIMEOUT_MS); parent.postMessage({ pluginMessage: { type: 'SCAN', enabledIds: enabledIds } }, '*'); } function onScanFailed(text) { setScanning(false); showCheckPanel(); var err = document.getElementById('scan-error'); if (err) { err.textContent = text + ' Попробуйте ещё раз.'; err.hidden = false; } announce('Ошибка сканирования'); } function hideScanError() { var err = document.getElementById('scan-error'); if (err) err.hidden = true; } document.getElementById('btn-scan').onclick = startScan; function setScanning(on) { var btn = document.getElementById('btn-scan'); var btnAgain = document.getElementById('btn-scan-again'); btn.disabled = on; if (btnAgain) btnAgain.disabled = on; btn.innerHTML = on ? '<span class="spinner"></span>Сканирование…' : 'Сканировать'; if (on) announce('Идёт сканирование…'); } function announce(text) { var el = document.getElementById('sr-status'); if (el) el.textContent = text; } // ─── Results rendering ──────────────────────────────────────────────────── var WAND_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + '<path stroke="none" d="M0 0h24v24H0z" fill="none"/>' + '<path d="M6 21l15 -15l-3 -3l-15 15l3 3" />' + '<path d="M15 6l3 3" />' + '<path d="M9 3l1 1" />' + '<path d="M17 11l1 1" />' + '<path d="M3 9l1 1" />' + '<path d="M13 3l0 .01" />' + '<path d="M21 13l0 .01" />' + '</svg>'; function renderResults() { var toolbar = document.getElementById('toolbar'); if (toolbar) toolbar.style.display = 'none'; var tabBarWrap = document.querySelector('.tab-bar-wrap'); if (tabBarWrap) tabBarWrap.style.display = 'none'; var panelCheck = document.getElementById('panel-check'); if (panelCheck) panelCheck.classList.add('hidden'); var pageResults = document.getElementById('page-results'); if (pageResults) pageResults.classList.remove('hidden'); var pageResultsBody = document.getElementById('page-results-body'); var emptyState = document.getElementById('page-results-empty-state'); // Показываем пустое состояние если нет нарушений if (!allViolations.length) { if (pageResultsBody) pageResultsBody.classList.add('hidden'); if (emptyState) { emptyState.classList.remove('hidden'); // Обновляем количество проверенных слоев var layersText = 'Проверено ' + totalNodesChecked + ' ' + plural(totalNodesChecked, 'слой', 'слоя', 'слоев'); var subtitle = document.getElementById('empty-state-layers-count'); if (subtitle) subtitle.textContent = layersText; } } else { if (emptyState) emptyState.classList.add('hidden'); if (pageResultsBody) pageResultsBody.classList.remove('hidden'); renderResultCards(allViolations); } announce(allViolations.length ? 'Сканирование завершено: ' + allViolations.length + ' ' + plural(allViolations.length, 'нарушение', 'нарушения', 'нарушений') : 'Сканирование завершено: ошибок нет'); } // ─── Result cards (страница результатов) ────────────────────────────────── // Нарушения приходят с бэкенда уже сгруппированными по (правило + слово-контекст), // каждое — со списком instances [{nodeId, nodeName, matchIndex}]. Повторной // группировки в UI нет. Карточки правил сортируются: сначала error, потом warning. function renderResultCards(violations) { var body = document.getElementById('page-results-body'); if (!body) return; body.innerHTML = ''; // Сначала сгруппировать по ruleId var ruleMap = {}; violations.forEach(function(v) { if (!ruleMap[v.ruleId]) { ruleMap[v.ruleId] = []; } ruleMap[v.ruleId].push(v); }); // Затем сгруппировать по groupId var groupedCards = {}; Object.keys(ruleMap).forEach(function(ruleId) { var rule = allRules.find(function(r) { return r.id === ruleId; }); var groupId = rule && rule.groupId ? rule.groupId : ruleId; if (!groupedCards[groupId]) { groupedCards[groupId] = { groupId: groupId, rules: [] }; } groupedCards[groupId].rules.push({ ruleId: ruleId, items: ruleMap[ruleId], rule: rule }); }); // Отсортировать группы по severity (сначала error) var groups = Object.keys(groupedCards); groups.sort(function(a, b) { var ra = groupedCards[a].rules[0].items[0].severity === 'error' ? 0 : 1; var rb = groupedCards[b].rules[0].items[0].severity === 'error' ? 0 : 1; return ra - rb; }); groups.forEach(function(groupId) { var cardData = groupedCards[groupId]; var cardRules = cardData.rules; // Определить тип карточки: простая (1 правило) или сложная (много правил) var isCompound = cardRules.length > 1; var total = 0; var fixableItems = []; cardRules.forEach(function(ruleGroup) { total += ruleGroup.items.reduce(function(sum, v) { return sum + (v.count || 1); }, 0); ruleGroup.items.forEach(function(v) { if (v.canFix) fixableItems.push({ ruleId: ruleGroup.ruleId, match: v.match, instances: v.instances || [] }); }); }); var isCollapsed = !!collapsedRules[groupId]; var hasFixes = fixableItems.length > 0; var card = document.createElement('div'); card.className = 'result-card'; var header = document.createElement('div'); header.className = 'card-header' + (isCollapsed ? ' is-collapsed' : ''); // Заголовок карточки var title = document.createElement('span'); title.className = 'card-title'; // Для простых карточек - имя правила, для сложных - общий заголовок if (isCompound) { // Использовать категорию или имя первого правила // Для группы 'units' always show "Сокращения" title.textContent = cardRules.some(function(r) { return r.rule && r.rule.groupId === 'units'; }) ? 'Сокращения' : (cardRules[0].rule ? cardRules[0].rule.short : 'Группа правил'); title.title = cardRules.map(function(r) { return r.rule ? r.rule.short : ''; }).join(', '); } else { // Для простой карточки проверяем groupId правила var rule = cardRules[0].rule; title.textContent = rule && rule.groupId === 'units' ? 'Сокращения' : (cardRules[0].items[0].short || cardRules[0].items[0].description); title.title = cardRules[0].items[0].description; } var counter = document.createElement('span'); counter.className = 'card-counter'; counter.textContent = total; header.appendChild(title); header.appendChild(counter); // Кнопка сворачивания карточки var collapseBtn = document.createElement('button'); collapseBtn.className = 'card-collapse-btn'; collapseBtn.innerHTML = CHEVRON_ICON; collapseBtn.setAttribute('aria-label', isCollapsed ? 'Развернуть' : 'Свернуть'); collapseBtn.title = isCollapsed ? 'Развернуть' : 'Свернуть'; collapseBtn.onclick = function(e) { e.stopPropagation(); toggleRuleCollapsed(groupId); }; header.appendChild(collapseBtn); // Кнопка "Исправить все" if (hasFixes) { var fixAllBtn = document.createElement('button'); fixAllBtn.className = 'card-fix-all-btn'; fixAllBtn.innerHTML = WAND_ICON + '<span>Исправить все</span>'; fixAllBtn.onclick = function(e) { e.stopPropagation(); // Для группы 'units' отправляем отдельные запросы для каждого правила if (groupId === 'units') { cardRules.forEach(function(ruleGroup) { var rule = ruleGroup.rule; if (rule && rule.id) { parent.postMessage({ pluginMessage: { type: 'AUTOFIX_ALL', ruleId: rule.id, items: ruleGroup.items.map(function(v) { return { match: v.match, targets: v.instances }; }) }}, '*'); } }); } else { parent.postMessage({ pluginMessage: { type: 'AUTOFIX_ALL', ruleId: groupId, items: fixableItems.map(function(v) { return { match: v.match, targets: v.instances }; }) }}, '*'); } }; header.appendChild(fixAllBtn); } card.appendChild(header); var cardBody = document.createElement('div'); cardBody.className = 'card-body' + (isCollapsed ? ' hidden' : ''); // Список всех нарушений без подкатегорий cardRules.forEach(function(ruleGroup) { ruleGroup.items.forEach(function(v) { var instances = Array.isArray(v.instances) ? v.instances : []; if (instances.length === 1) cardBody.appendChild(makeCardRow(v)); else if (instances.length > 1) cardBody.appendChild(makeGroupRow(v)); }); }); card.appendChild(cardBody); body.appendChild(card); }); } // Сворачивание/разворачивание карточки правила целиком. Состояние запоминаем // между сканами — шлём на бэкенд (clientStorage), он при старте вернёт его // в COLLAPSED_RULES. Используем Set-подобную семантику: объект-словарь. function toggleRuleCollapsed(ruleId) { if (collapsedRules[ruleId]) delete collapsedRules[ruleId]; else collapsedRules[ruleId] = true; saveCollapsedRules(); renderResultCards(allViolations); } function saveCollapsedRules() { parent.postMessage({ pluginMessage: { type: 'SAVE_RULE_COLLAPSED', collapsed: Object.keys(collapsedRules) }}, '*'); } function sendAutofix(v, targets) { parent.postMessage({ pluginMessage: { type: 'AUTOFIX', ruleId: v.ruleId, match: v.match, targets: targets }}, '*'); } function sendFocus(nodeIds) { parent.postMessage({ pluginMessage: { type: 'FOCUS', nodeIds: nodeIds } }, '*'); } function buildBeforeEl(v) { var pre = v.context ? v.context.pre : ''; var post = v.context ? v.context.post : ''; var before = document.createElement('span'); before.className = 'diff-before'; if (pre) before.appendChild(document.createTextNode(showSpecial(pre))); var beforeHit = document.createElement('mark'); beforeHit.className = 'diff-hit'; beforeHit.textContent = showSpecial(v.match); before.appendChild(beforeHit); if (post) before.appendChild(document.createTextNode(showSpecial(post))); return before; } function buildAfterEl(v) { var pre = v.context ? v.context.pre : ''; var post = v.context ? v.context.post : ''; var after = document.createElement('span'); after.className = 'diff-after'; if (v.afterMatch != null) { if (v.afterMatch === '') { if (pre) after.appendChild(document.createTextNode(showSpecial(pre))); if (post) after.appendChild(document.createTextNode(showSpecial(post))); } else { if (pre) after.appendChild(document.createTextNode(showSpecial(pre))); var afterHit = document.createElement('span'); afterHit.className = 'diff-after-hit'; afterHit.textContent = showSpecial(v.afterMatch); after.appendChild(afterHit); if (post) after.appendChild(document.createTextNode(showSpecial(post))); } } else { after.className += ' is-fix-hint'; after.textContent = v.fix; } return after; } function buildArrowEl() { var arrow = document.createElement('span'); arrow.className = 'diff-arrow'; arrow.textContent = '→'; return arrow; } function buildDiffEl(v) { var diff = document.createElement('span'); diff.className = 'row-diff'; diff.appendChild(buildBeforeEl(v)); diff.appendChild(buildArrowEl()); diff.appendChild(buildAfterEl(v)); return diff; } function buildFixOnlyEl(v) { var el = document.createElement('span'); el.className = 'row-diff'; el.appendChild(buildArrowEl()); el.appendChild(buildAfterEl(v)); return el; } function makeCardRow(v) { var inst = v.instances[0]; var row = document.createElement('div'); row.className = 'card-row'; row.tabIndex = 0; row.setAttribute('role', 'button'); row.appendChild(buildDiffEl(v)); if (v.canFix) { var fixBtn = document.createElement('button'); fixBtn.className = 'row-fix-btn'; fixBtn.innerHTML = WAND_ICON; var fixLabel = 'Исправить'; fixBtn.title = fixLabel; fixBtn.setAttribute('aria-label', fixLabel); fixBtn.onclick = function(e) { e.stopPropagation(); sendAutofix(v, [inst]); }; row.appendChild(fixBtn); } row.onclick = function() { sendFocus([inst.nodeId]); }; row.onkeydown = function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); row.click(); } }; return row; } var CHEVRON_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 6l6 6l-6 6" /></svg>'; function makeGroupRow(v) { var key = v.ruleId + '|' + v.match + '|' + (v.context ? v.context.pre + v.context.post : ''); var wrap = document.createElement('div'); wrap.className = 'card-row-group-wrap'; var row = document.createElement('div'); row.className = 'card-row card-row-group'; row.tabIndex = 0; row.setAttribute('role', 'button'); var errorText = buildBeforeEl(v); errorText.classList.add('row-error-text'); row.appendChild(errorText); var badge = document.createElement('span'); badge.className = 'row-count-badge'; badge.textContent = v.instances.length; row.appendChild(badge); if (v.canFix) { var fixBtn = document.createElement('button'); fixBtn.className = 'row-fix-btn'; fixBtn.innerHTML = WAND_ICON; var fixLabel = 'Исправить всё'; fixBtn.title = fixLabel; fixBtn.setAttribute('aria-label', fixLabel); fixBtn.onclick = function(e) { e.stopPropagation(); sendAutofix(v, v.instances); }; row.appendChild(fixBtn); } var chevron = document.createElement('span'); chevron.className = 'row-chevron'; chevron.innerHTML = CHEVRON_ICON; row.appendChild(chevron); var instancesEl = document.createElement('div'); instancesEl.className = 'group-instances'; v.instances.forEach(function(inst) { var instRow = document.createElement('div'); instRow.className = 'card-row card-row-instance'; instRow.tabIndex = 0; instRow.setAttribute('role', 'button'); instRow.appendChild(buildFixOnlyEl(v)); if (v.canFix) { var instFixBtn = document.createElement('button'); instFixBtn.className = 'row-fix-btn'; instFixBtn.innerHTML = WAND_ICON; instFixBtn.title = 'Исправить'; instFixBtn.setAttribute('aria-label', 'Исправить'); instFixBtn.onclick = function(e) { e.stopPropagation(); sendAutofix(v, [inst]); }; instRow.appendChild(instFixBtn); } instRow.onclick = function() { sendFocus([inst.nodeId]); }; instRow.onkeydown = function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); instRow.click(); } }; instancesEl.appendChild(instRow); }); function setExpanded(expanded) { expandedGroups[key] = expanded; row.classList.toggle('is-expanded', expanded); instancesEl.hidden = !expanded; } setExpanded(!!expandedGroups[key]); row.onclick = function() { setExpanded(!expandedGroups[key]); }; row.onkeydown = function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); row.click(); } }; wrap.appendChild(row); wrap.appendChild(instancesEl); return wrap; } // ─── Settings panel ─────────────────────────────────────────────────────── var INFO_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><circle cx="12" cy="12" r="9"/><line x1="12" y1="11" x2="12" y2="16"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>'; function showRuleTooltip(anchorEl, text) { var tip = document.getElementById('rule-tooltip'); tip.textContent = text; tip.style.display = 'block'; var rect = anchorEl.getBoundingClientRect(); var tipRect = tip.getBoundingClientRect(); var left = Math.max(4, rect.right - tipRect.width); var top = rect.top - tipRect.height - 6; if (top < 4) top = rect.bottom + 6; tip.style.left = left + 'px'; tip.style.top = top + 'px'; } function hideRuleTooltip() { document.getElementById('rule-tooltip').style.display = 'none'; } function buildSettingsPanel() { var list = document.getElementById('settings-list'); list.innerHTML = ''; // Сгруппировать правила по groupId var groupedRules = {}; allRules.forEach(function(r) { var groupId = r.groupId || r.id; if (!groupedRules[groupId]) { groupedRules[groupId] = { groupId: groupId, category: r.category, rules: [] }; } groupedRules[groupId].rules.push(r); }); // Отобразить группы Object.keys(groupedRules).forEach(function(groupId) { var group = groupedRules[groupId]; if (group.rules.length === 0) return; // Заголовок категории if (group.category) { var cat = document.createElement('div'); cat.className = 'settings-cat'; cat.textContent = group.category; list.appendChild(cat); } // Общий чекбокс для всей группы var groupRow = document.createElement('div'); groupRow.className = 'settings-rule'; var left = document.createElement('div'); left.className = 'rule-left'; var switchWrap = document.createElement('label'); switchWrap.className = 'rule-switch'; var rcb = document.createElement('input'); rcb.type = 'checkbox'; rcb.dataset.groupId = groupId; // Чекбокс активен, если ВСЕ правила в группе включены var allEnabled = group.rules.every(function(r) { return enabledIds.indexOf(r.id) !== -1; }); rcb.checked = allEnabled; rcb.onchange = function() { var isChecked = rcb.checked; // Включить/выключить все правила в группе group.rules.forEach(function(r) { var idx = enabledIds.indexOf(r.id); if (isChecked && idx === -1) enabledIds.push(r.id); if (!isChecked && idx !== -1) enabledIds.splice(idx, 1); }); updateToggleAllBtn(); saveSettings(); updateSettingsTabBadge(); }; var track = document.createElement('span'); track.className = 'switch-track'; var thumb = document.createElement('span'); thumb.className = 'switch-thumb'; track.appendChild(thumb); switchWrap.appendChild(rcb); switchWrap.appendChild(track); var nameEl = document.createElement('span'); nameEl.className = 'rule-name'; // Показать имя основного правила или название группы nameEl.textContent = group.rules[0].short; left.appendChild(switchWrap); left.appendChild(nameEl); var icon = document.createElement('span'); icon.className = 'rule-info-icon'; icon.innerHTML = INFO_ICON; // Показать описание группы с полным списком правил var descriptions; if (groupId === 'units') { descriptions = 'Правила сокращений единиц измерения (ГБ, МБ, КБ, ТБ, сек, мс, мин, ч)'; } else { descriptions = group.rules.map(function(r) { return '• ' + r.short + ' — ' + r.description; }).join('\n'); } icon.onmouseenter = function() { showRuleTooltip(icon, descriptions); }; icon.onmouseleave = hideRuleTooltip; groupRow.appendChild(left); groupRow.appendChild(icon); groupRow.onmouseleave = hideRuleTooltip; list.appendChild(groupRow); }); updateToggleAllBtn(); } function syncRuleSwitches() { // Обновить чекбоксы групп (по groupId) var groupedRules = {}; allRules.forEach(function(r) { var groupId = r.groupId || r.id; if (!groupedRules[groupId]) { groupedRules[groupId] = { rules: [] }; } groupedRules[groupId].rules.push(r); }); Object.keys(groupedRules).forEach(function(groupId) { var group = groupedRules[groupId]; var groupCb = document.querySelector('[data-group-id="' + groupId + '"]'); if (groupCb) { var allEnabled = group.rules.every(function(r) { return enabledIds.indexOf(r.id) !== -1; }); groupCb.checked = allEnabled; } }); } function updateToggleAllBtn() { var btn = document.getElementById('btn-toggle-all'); btn.textContent = enabledIds.length === allRules.length ? 'Выключить все' : 'Включить все'; } function updateSettingsTabBadge() { var badge = document.getElementById('settings-tab-badge'); var total = allRules.length; if (!total) { badge.style.display = 'none'; return; } badge.style.display = ''; badge.textContent = enabledIds.length + '/' + total; } function saveSettings() { parent.postMessage({ pluginMessage: { type: 'SAVE_SETTINGS', enabledIds: enabledIds } }, '*'); } document.getElementById('btn-toggle-all').onclick = function() { var allOn = enabledIds.length === allRules.length; enabledIds = allOn ? [] : allRules.map(function(r) { return r.id; }); syncRuleSwitches(); updateToggleAllBtn(); saveSettings(); updateSettingsTabBadge(); }; // ─── Helpers ────────────────────────────────────────────────────────────── function showSpecial(str) { if (!str) return ''; return str .replace(/ /g, '·') // thin space → middle dot .replace(/ /g, '␣') // nbsp → open box symbol .replace(/\n/g, '↵'); // newline → return symbol } function plural(n, one, few, many) { var mod10 = n % 10, mod100 = n % 100; if (mod10 === 1 && mod100 !== 11) return one; if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return few; return many; } </script> </body> </html>