/
vanamingo
/
math_texteditor
Обзор
Документация
Войти
/
vanamingo
/
math_texteditor
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
app.js
490 строк
16 KB
MM
INIT
07 июн 2026, 23:19
07 июн 2026, 23:19
aaa6060
Код
Авторство
О чём код?
/* ============================================================ КБ Редактор — app.js Quill + KaTeX integration for engineering document editor ============================================================ */ // ===== Custom Quill Blots for Formulas ===== // Block formula blot (displayed on its own line, centered) const BlockEmbed = Quill.import('blots/block/embed'); class FormulaBlock extends BlockEmbed { static create(laTeX) { const node = super.create(); node.setAttribute('data-latex', laTeX); node.classList.add('kb-formula-block'); node.setAttribute('contenteditable', 'false'); try { katex.render(laTeX, node, { displayMode: true, throwOnError: false, output: 'html' }); } catch (e) { node.textContent = laTeX; } return node; } static value(node) { return node.getAttribute('data-latex') || ''; } format(name, value) { if (name === 'formula-block' && value) { this.domNode.setAttribute('data-latex', value); this.domNode.innerHTML = ''; try { katex.render(value, this.domNode, { displayMode: true, throwOnError: false, output: 'html' }); } catch (e) { this.domNode.textContent = value; } } } } FormulaBlock.blotName = 'formula-block'; FormulaBlock.className = 'kb-formula-block'; FormulaBlock.tagName = 'div'; Quill.register(FormulaBlock, true); // Inline formula blot (displayed within text) const InlineEmbed = Quill.import('blots/embed'); class FormulaInline extends InlineEmbed { static create(laTeX) { const node = super.create(); node.setAttribute('data-latex', laTeX); node.classList.add('kb-formula-inline'); try { katex.render(laTeX, node, { displayMode: false, throwOnError: false, output: 'html' }); } catch (e) { node.textContent = laTeX; } return node; } static value(node) { return node.getAttribute('data-latex') || ''; } } FormulaInline.blotName = 'formula-inline'; FormulaInline.className = 'kb-formula-inline'; FormulaInline.tagName = 'span'; Quill.register(FormulaInline, true); // ===== Quill Editor Initialization ===== const quill = new Quill('#editor', { theme: 'snow', placeholder: 'Начните ввод текста...', modules: { toolbar: { container: '#toolbar' }, history: { delay: 1000, maxStack: 100 } } }); // Make quill globally accessible for undo/redo buttons window.quill = quill; // ===== Formula Modal Logic ===== const formulaModal = document.getElementById('formula-modal'); const latexInput = document.getElementById('latex-input'); const formulaPreview = document.getElementById('formula-preview'); const modalTitle = document.getElementById('modal-title'); const modalInsertBtn = document.getElementById('modal-insert-btn'); const modalCancelBtn = document.getElementById('modal-cancel-btn'); const modalCloseBtn = document.getElementById('modal-close-btn'); const formulaBtn = document.getElementById('formula-btn'); const formulaInlineBtn = document.getElementById('formula-inline-btn'); let currentFormulaMode = 'block'; // 'block' or 'inline' let editingFormulaIndex = -1; // -1 means new formula function openFormulaModal(mode, editIndex = -1, editValue = '') { currentFormulaMode = mode; editingFormulaIndex = editIndex; modalTitle.textContent = mode === 'block' ? 'Вставка формулы (блочная)' : 'Вставка формулы (инлайн)'; latexInput.value = editIndex >= 0 && editValue ? editValue : ''; updatePreview(); formulaModal.style.display = 'flex'; setTimeout(() => latexInput.focus(), 100); } function closeFormulaModal() { formulaModal.style.display = 'none'; latexInput.value = ''; formulaPreview.innerHTML = ''; editingFormulaIndex = -1; quill.focus(); } function updatePreview() { const latex = latexInput.value.trim(); if (!latex) { formulaPreview.innerHTML = '<span style="color:#6b7c8d;font-style:italic;">Введите LaTeX-выражение для предпросмотра</span>'; return; } try { katex.render(latex, formulaPreview, { displayMode: currentFormulaMode === 'block', throwOnError: true, output: 'html' }); } catch (e) { try { katex.render(latex, formulaPreview, { displayMode: currentFormulaMode === 'block', throwOnError: false, output: 'html' }); } catch (e2) { formulaPreview.innerHTML = '<span class="katex-error">Ошибка: ' + e.message + '</span>'; } } } function insertFormula() { const latex = latexInput.value.trim(); if (!latex) { closeFormulaModal(); return; } const selection = quill.getSelection(true); const index = selection ? selection.index : 0; if (editingFormulaIndex >= 0) { // Editing existing formula — delete and re-insert quill.deleteText(editingFormulaIndex, 1); if (currentFormulaMode === 'block') { quill.insertEmbed(editingFormulaIndex, 'formula-block', latex); quill.setSelection(editingFormulaIndex + 1, 0); } else { quill.insertEmbed(editingFormulaIndex, 'formula-inline', latex); quill.setSelection(editingFormulaIndex + 1, 0); } } else { // New formula if (currentFormulaMode === 'block') { // Ensure we are on a new line const [line] = quill.getLine(index); if (line && line.length() > 1) { quill.insertText(index, '\n'); quill.insertEmbed(index + 1, 'formula-block', latex); quill.insertText(index + 2, '\n'); quill.setSelection(index + 3, 0); } else { quill.insertEmbed(index, 'formula-block', latex); quill.insertText(index + 1, '\n'); quill.setSelection(index + 2, 0); } } else { quill.insertEmbed(index, 'formula-inline', latex); quill.setSelection(index + 1, 0); } } closeFormulaModal(); } // Event listeners for modal formulaBtn.addEventListener('click', () => openFormulaModal('block')); formulaInlineBtn.addEventListener('click', () => openFormulaModal('inline')); modalInsertBtn.addEventListener('click', insertFormula); modalCancelBtn.addEventListener('click', closeFormulaModal); modalCloseBtn.addEventListener('click', closeFormulaModal); latexInput.addEventListener('input', updatePreview); formulaModal.addEventListener('click', (e) => { if (e.target === formulaModal) closeFormulaModal(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && formulaModal.style.display === 'flex') closeFormulaModal(); }); latexInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); insertFormula(); } }); // ===== Click on formula in editor to edit it ===== quill.root.addEventListener('click', (e) => { const blockFormula = e.target.closest('.kb-formula-block'); if (blockFormula) { const blot = Quill.find(blockFormula); if (blot) { const offset = quill.getIndex(blot); const latex = blockFormula.getAttribute('data-latex') || ''; openFormulaModal('block', offset, latex); } return; } const inlineFormula = e.target.closest('.kb-formula-inline'); if (inlineFormula) { const blot = Quill.find(inlineFormula); if (blot) { const offset = quill.getIndex(blot); const latex = inlineFormula.getAttribute('data-latex') || ''; openFormulaModal('inline', offset, latex); } } }); // ===== Symbol Palette Logic ===== document.querySelectorAll('.palette-btn').forEach(btn => { btn.addEventListener('click', () => { const symbol = btn.getAttribute('data-symbol'); if (!symbol) return; const selection = quill.getSelection(true); if (selection) { // Delete selected text first, then insert symbol quill.deleteText(selection.index, selection.length); quill.insertText(selection.index, symbol); quill.setSelection(selection.index + symbol.length, 0); } else { const length = quill.getLength(); quill.insertText(length - 1, symbol); quill.setSelection(length + symbol.length - 1, 0); } quill.focus(); }); }); // ===== Quick Formula Templates ===== const templates = [ { label: 'Дробь', latex: '\\frac{a}{b}' }, { label: 'Корень', latex: '\\sqrt{x}' }, { label: 'Квадрат', latex: 'x^{2}' }, { label: 'Индекс', latex: 'x_{i}' }, { label: 'Сумма', latex: '\\sum_{i=1}^{n} a_i' }, { label: 'Интеграл', latex: '\\int_{a}^{b} f(x)\\,dx' }, { label: 'Предел', latex: '\\lim_{x \\to \\infty} f(x)' }, { label: 'Вектор', latex: '\\vec{F} = m\\vec{a}' }, { label: 'Матрица 2×2', latex: '\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}' }, { label: 'σ ≤ [σ]', latex: '\\sigma_{\\max} \\leq [\\sigma]' }, { label: 'M/W', latex: '\\sigma = \\frac{M}{W}' }, { label: 'Кв. уравнение', latex: 'ax^{2} + bx + c = 0' }, { label: '∂f/∂x', latex: '\\frac{\\partial f}{\\partial x}' }, { label: 'Ур. Максвелла', latex: '\\nabla \\cdot \\vec{E} = \\frac{\\rho}{\\varepsilon_0}' }, { label: 'Пифагор', latex: 'a^{2} + b^{2} = c^{2}' }, { label: 'Эйлер', latex: 'e^{i\\pi} + 1 = 0' }, ]; function buildTemplates() { const modalBody = document.querySelector('.modal-body'); const templatesDiv = document.createElement('div'); templatesDiv.className = 'quick-templates'; templatesDiv.innerHTML = '<div class="quick-templates-title">Быстрые шаблоны</div><div class="quick-templates-grid"></div>'; const grid = templatesDiv.querySelector('.quick-templates-grid'); templates.forEach(tpl => { const btn = document.createElement('button'); btn.className = 'template-btn'; btn.textContent = tpl.label; btn.title = tpl.latex; btn.addEventListener('click', () => { latexInput.value = tpl.latex; updatePreview(); latexInput.focus(); }); grid.appendChild(btn); }); modalBody.appendChild(templatesDiv); } buildTemplates(); // ===== Keyboard shortcuts ===== quill.root.addEventListener('keydown', (e) => { // Ctrl+M: block formula if (e.key === 'm' && (e.ctrlKey || e.metaKey) && !e.shiftKey) { e.preventDefault(); openFormulaModal('block'); } // Ctrl+Shift+M: inline formula if (e.key === 'M' && (e.ctrlKey || e.metaKey) && e.shiftKey) { e.preventDefault(); openFormulaModal('inline'); } }); // ===== Insert demo content ===== setTimeout(() => { quill.setContents([]); // Build demo content programmatically using Quill API quill.insertText(0, 'Техническое задание', 'header', 2); let pos = quill.getLength(); quill.insertText(pos, '\nДанный документ подготовлен в конструкторском бюро с использованием встроенного математического редактора. Для вставки формул используйте кнопки '); pos = quill.getLength(); quill.insertEmbed(pos, 'formula-inline', '\\text{f(x)}'); pos = quill.getLength(); quill.insertText(pos, ' (блочная) и '); pos = quill.getLength(); quill.insertEmbed(pos, 'formula-inline', 'x^2'); pos = quill.getLength(); quill.insertText(pos, ' (инлайн) на панели инструментов, либо сочетания клавиш '); pos = quill.getLength(); quill.insertText(pos, 'Ctrl+M', 'bold', true); pos = quill.getLength(); quill.insertText(pos, ' и '); pos = quill.getLength(); quill.insertText(pos, 'Ctrl+Shift+M', 'bold', true); pos = quill.getLength(); quill.insertText(pos, '.\n\nРасчёт напряжений'); pos = quill.getLength(); quill.formatText(pos - 'Расчёт напряжений'.length, 'Расчёт напряжений'.length, 'header', 3); pos = quill.getLength(); quill.insertText(pos, '\nМаксимальное напряжение в сечении балки при изгибе:'); pos = quill.getLength(); quill.insertText(pos, '\n'); pos = quill.getLength(); quill.insertEmbed(pos, 'formula-block', '\\sigma_{\\max} = \\frac{M_{\\max}}{W_z}'); pos = quill.getLength(); quill.insertText(pos, '\nГде '); pos = quill.getLength(); quill.insertEmbed(pos, 'formula-inline', 'M_{\\max}'); pos = quill.getLength(); quill.insertText(pos, ' — максимальный изгибающий момент, '); pos = quill.getLength(); quill.insertEmbed(pos, 'formula-inline', 'W_z'); pos = quill.getLength(); quill.insertText(pos, ' — момент сопротивления сечения.'); pos = quill.getLength(); quill.insertText(pos, '\n\nУсловие прочности'); pos = quill.getLength(); quill.formatText(pos - 'Условие прочности'.length, 'Условие прочности'.length, 'header', 3); pos = quill.getLength(); quill.insertText(pos, '\nУсловие прочности при изгибе:'); pos = quill.getLength(); quill.insertText(pos, '\n'); pos = quill.getLength(); quill.insertEmbed(pos, 'formula-block', '\\sigma_{\\max} \\leq [\\sigma] = \\frac{\\sigma_{\\text{пред}}}{n}'); pos = quill.getLength(); quill.insertText(pos, '\nгде '); pos = quill.getLength(); quill.insertEmbed(pos, 'formula-inline', 'n'); pos = quill.getLength(); quill.insertText(pos, ' — коэффициент запаса прочности, '); pos = quill.getLength(); quill.insertEmbed(pos, 'formula-inline', '\\sigma_{\\text{пред}}'); pos = quill.getLength(); quill.insertText(pos, ' — предельное напряжение.'); pos = quill.getLength(); quill.insertText(pos, '\n\nДля пластичных материалов:'); pos = quill.getLength(); quill.insertText(pos, '\n'); pos = quill.getLength(); quill.insertEmbed(pos, 'formula-block', '\\sigma_{\\text{пред}} = \\sigma_{\\text{т}}'); pos = quill.getLength(); quill.insertText(pos, '\nДля хрупких материалов:'); pos = quill.getLength(); quill.insertText(pos, '\n'); pos = quill.getLength(); quill.insertEmbed(pos, 'formula-block', '\\sigma_{\\text{пред}} = \\sigma_{\\text{в}}'); pos = quill.getLength(); quill.insertText(pos, '\n\nПример расчёта касательных напряжений:'); pos = quill.getLength(); quill.insertText(pos, '\n'); pos = quill.getLength(); quill.insertEmbed(pos, 'formula-block', '\\tau = \\frac{Q \\cdot S_x}{I_x \\cdot b}'); pos = quill.getLength(); quill.insertText(pos, '\n\nФормула Эйлера для критической силы:'); pos = quill.getLength(); quill.insertText(pos, '\n'); pos = quill.getLength(); quill.insertEmbed(pos, 'formula-block', 'F_{\\text{кр}} = \\frac{\\pi^2 E I_{\\min}}{(\\mu l)^2}'); pos = quill.getLength(); quill.insertText(pos, '\n\nНажмите на любую формулу для редактирования. Используйте палитру символов выше для быстрой вставки математических знаков.\n'); pos = quill.getLength(); quill.setSelection(0, 0); }, 150); // ===== Utility functions ===== function getEditorHTML() { return quill.root.innerHTML; } function getEditorText() { return quill.getText(); } window.getEditorHTML = getEditorHTML; window.getEditorText = getEditorText;