mod: css и js вынесены во внешние файлы (будут кашироваться, что ускорит загрузку)

This commit is contained in:
2026-01-16 17:06:16 +03:00
parent b50a3202a8
commit a0d7392cf7
5 changed files with 421 additions and 379 deletions

96
public/static/js/base.js Normal file
View File

@@ -0,0 +1,96 @@
(function () {
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const logoImg = document.getElementById('logo-img');
const navbar = document.getElementById('main-navbar');
// --- АВТОМАТИЧЕСКОЕ ПЕРЕКЛЮЧЕНИЕ ТЕМЫ (Dark/Light) ---
function updateTheme(e) {
const theme = e.matches ? 'dark' : 'light';
document.documentElement.setAttribute('data-bs-theme', theme);
}
// --- ОБНОВЛЕНИЕ ЛОГОТИПА ПРИ СКРОЛЛЕ И СМЕНЕ ТЕМЫ ---
function updateLogo() {
const isDark = darkModeMediaQuery.matches;
// Используем getBoundingClientRect для определения позиции контента
if (document.getElementById('content-container').getBoundingClientRect().top < 78) {
navbar.classList.add('navbar-scrolled');
logoImg.src = isDark ? logoImg.dataset.srcDarkCompact : logoImg.dataset.srcLightCompact;
} else {
navbar.classList.remove('navbar-scrolled');
logoImg.src = isDark ? logoImg.dataset.srcDark : logoImg.dataset.srcLight;
}
}
// Инициализация
updateTheme(darkModeMediaQuery);
updateLogo();
document.addEventListener('DOMContentLoaded', updateLogo);
// Слушаем скролл
window.addEventListener('scroll', updateLogo);
// Слушаем смену темы
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateLogo);
// --- КУКИ ---
const COOKIE_KEY = 'cookie_consent';
const TTL_MS = 60 * 1000; // 1 минута для отладки (потом поставить 90 дней: 90 * 24 * 60 * 60 * 1000 = 7776000000)
const banner = document.getElementById('cookie-banner');
const acceptButton = document.getElementById('cookie-accept');
function loadCounters() {
console.log("Загрузка счетчиков (Яндекс, Google)...");
// Код Яндекс.Метрики
// (function(m,e,t,r,i,k,a){...})(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
// ym(XXXXXX, "init", {...});
// Код Google Analytics
// window.dataLayer = window.dataLayer || [];
// function gtag(){dataLayer.push(arguments);}
// gtag('js', new Date());
// gtag('config', 'G-XXXXXXXXXX');
// Код Top.Mail.Ru
// (function(w, d, c) { ... })(window, document, "topmailru");
// и т.д.
// alert("Отладка. Счетчики загружены (здесь должен быть реальный код счетчиков).");
}
function checkConsent() {
const stored = localStorage.getItem(COOKIE_KEY);
if (!stored) return false;
try {
const data = JSON.parse(stored);
const now = Date.now();
// Проверяем, не истек ли срок
if (now - data.timestamp > TTL_MS) {
localStorage.removeItem(COOKIE_KEY);
return false;
}
return true;
} catch (e) {
return false;
}
}
if (checkConsent()) {
loadCounters();
} else {
banner.style.display = 'block';
}
acceptButton.addEventListener('click', function () {
const data = {
value: true,
timestamp: Date.now()
};
localStorage.setItem(COOKIE_KEY, JSON.stringify(data));
banner.style.display = 'none';
loadCounters();
});
})();

99
public/static/js/index.js Normal file
View File

@@ -0,0 +1,99 @@
// Импортируем из локального бандла (относительный путь)
import {
EditorView,
EditorState,
lineNumbers,
highlightActiveLineGutter,
highlightWhitespace,
highlightTrailingWhitespace,
drawSelection,
keymap,
highlightSpecialChars,
html,
oneDark,
syntaxHighlighting,
defaultHighlightStyle,
bracketMatching,
defaultKeymap,
Compartment
} from "../codemirror/editor.js";
const resultWrapper = document.getElementById('cm-result-wrapper');
const themeCompartment = new Compartment();
function getTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? oneDark : [];
}
// Словарь названий для спецсимволов
const charNames = {
0x00A0: "NoBreakable Space (неразрывный пробел — &nbsp;)",
0x00AD: "Soft Hyphen (мягкий перенос — &shy;)",
0x2002: "En Space (полужирный пробел — &ensp;)",
0x2003: "Em Space (жирный пробел — &emsp;)",
0x2007: "Figure Space (цифровой пробел — &numsp;)",
0x2008: "Punctuation Space (пунктуационный пробел — &puncsp;)",
0x2009: "Thin Space (тонкий пробел — &thinsp;)",
0x200A: "Hair Space (толщина волоса — &hairsp;)",
0x200B: "Negative Space (негативный пробел — &NegativeThinSpace;)",
0x200C: "Zero Width Non-Joiner (пробел нулевой ширины, без объединения — &zwj;)",
0x200D: "Zero Width Joiner (пробел нулевой ширины, с объединением — &zwnj;)",
0x200E: "Left-to-Right Mark (изменить направление текста на слева-направо — &lrm;)",
0x200F: "Right-to-Left Mark (изменить направление текста на справа-налево — &rlm;)",
0x205F: "Medium Mathematical Space (средний пробел — &MediumSpace;)",
0x2060: "NoBreak (без разрыва — &NoBreak;)",
0x2062: "Invisible Times (невидимое умножение для семантической разметки математических выражений — &InvisibleTimes;)",
0x2063: "Invisible Comma (невидимая запятая для семантической разметки математических выражений — &InvisibleComma;)",
};
const resultState = EditorState.create({
doc: "Здесь появится результат...",
extensions: [
lineNumbers(),
highlightActiveLineGutter(),
// Подсветка NBSP и других специальных пробелов
highlightSpecialChars({
specialChars: /[\u2002\u00AD\u2003\u2007\u2009\u00a0\u200A\u200B\u200C\u200D\u200E\u200F\u205F\u2060\u2062\u2063]/,
addSpecialChars: true,
render: (code) => {
let span = document.createElement("span");
span.textContent = "•";
span.style.background = "#ff000044"; // Полупрозрачный красный фон
span.style.color = "#ffff00"; // Желтый цвет точки
// Используем словарь для title
span.title = "U+" + code.toString(16).toUpperCase().padStart(4, '0') + " / " + (charNames[code] || "Special Char");
return span;
}
}),
highlightWhitespace(),
highlightTrailingWhitespace(),
drawSelection(),
syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
bracketMatching(),
keymap.of(defaultKeymap),
html(),
themeCompartment.of(getTheme()),
EditorState.readOnly.of(true)
]
});
const resultView = new EditorView({
state: resultState,
parent: resultWrapper
});
document.body.addEventListener('htmx:afterSwap', function (evt) {
if (evt.detail.target.id === 'result-area') {
const newContent = evt.detail.xhr.response;
resultView.dispatch({
changes: {from: 0, to: resultView.state.doc.length, insert: newContent}
});
}
});
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
resultView.dispatch({
effects: themeCompartment.reconfigure(getTheme())
});
});