feat(lab-graph): введённые функции — редактируемое KaTeX-поле

Введённая функция показывается отрисованной формулой KaTeX прямо в строке;
клик по формуле → правка текста на месте (raw input + живое превью под полем),
клик мимо/blur → снова формула. Реализовано без MathQuill: .fn-field держит
<input> и .fn-math (KaTeX), класс has-math переключает отображение по фокусу.

- renderFnMath() рисует формулу в строке; _fnDisplay() решает режим (фокус+значение)
- focus/blur/mousedown-обработчики в _initGraphPanel (идемпотентно)
- живое превью .fn-preview теперь видно ТОЛЬКО при правке (:focus-within), цвет функции
- graphInsert/applyPreset/state-apply/clearAll/default-fn0 обновляют math-поле
- _katexInto() — общий безопасный рендер

Только фронт. node --check OK; логика вставки 5/5 (прошлый прогон).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-24 19:02:08 +03:00
parent 000e42f9b3
commit fa29332bcd
3 changed files with 72 additions and 26 deletions
+47 -16
View File
@@ -519,6 +519,7 @@ class GraphSim {
const el = document.getElementById(`fn${i}`);
if (el) { el.value = fn.expr; }
if (gSim) gSim.setFn(i, fn.expr, FN_COLORS[i]);
_fnDisplay(i);
});
}
);
@@ -531,7 +532,7 @@ class GraphSim {
gSim.onHover = updateInfoBar;
if (!document.getElementById('fn0').value.trim()) {
document.getElementById('fn0').value = 'sin(x)';
renderPreview(0);
renderPreview(0); _fnDisplay(0);
gSim.fit();
gSim.setFn(0, 'sin(x)', FN_COLORS[0]);
return;
@@ -574,22 +575,40 @@ class GraphSim {
.replace(/\*/g, '\\cdot ');
}
function _katexInto(el, latex) {
try { el.innerHTML = katex.renderToString(latex, { throwOnError: false, strict: false, displayMode: false }); return true; }
catch { el.innerHTML = ''; return false; }
}
/* Живое превью формулы под полем — показывается ТОЛЬКО пока строка в правке. */
function renderPreview(idx) {
const inp = document.getElementById('fn' + idx);
const prev = document.getElementById('fn' + idx + '-prev');
if (!prev) return;
const raw = inp?.value?.trim() || '';
const src = raw || (inp?.getAttribute('placeholder') || ''); // пусто → показываем плейсхолдер-формулу
if (!src || typeof katex === 'undefined') {
prev.innerHTML = ''; prev.classList.remove('has-content', 'is-ph'); return;
}
try {
prev.innerHTML = katex.renderToString(toLatex(src), {
throwOnError: false, strict: false, displayMode: false,
});
prev.classList.add('has-content');
prev.classList.toggle('is-ph', !raw); // плейсхолдер — приглушённо
} catch { prev.innerHTML = ''; prev.classList.remove('has-content', 'is-ph'); }
if (!raw || typeof katex === 'undefined') { prev.innerHTML = ''; prev.classList.remove('has-content'); return; }
if (_katexInto(prev, toLatex(raw))) prev.classList.add('has-content');
else prev.classList.remove('has-content');
}
/* Введённая функция — отрисованной формулой KaTeX прямо в строке. */
function renderFnMath(idx) {
const inp = document.getElementById('fn' + idx);
const m = document.getElementById('fn' + idx + '-math');
if (!m || !inp) return;
const raw = inp.value.trim();
if (!raw || typeof katex === 'undefined') { m.innerHTML = ''; return; }
_katexInto(m, toLatex(raw));
}
/* Режим строки: не в фокусе и есть формула → показываем KaTeX; иначе — поле ввода. */
function _fnDisplay(idx) {
const inp = document.getElementById('fn' + idx);
const field = inp && inp.closest('.fn-field');
if (!field) return;
const showMath = (document.activeElement !== inp) && !!inp.value.trim() && typeof katex !== 'undefined';
if (showMath) { renderFnMath(idx); field.classList.add('has-math'); }
else field.classList.remove('has-math');
}
/* Вставка структуры формулы в активное поле (как редактор формул).
@@ -598,6 +617,7 @@ class GraphSim {
function graphInsert(token) {
const el = document.getElementById('fn' + _activeFnIdx) || document.getElementById('fn0');
if (!el) return;
const f = el.closest('.fn-field'); if (f) f.classList.remove('has-math'); // в режим правки
el.focus();
let ins = String(token || ''); let caretInTok = -1;
const bar = ins.indexOf('|');
@@ -610,19 +630,28 @@ class GraphSim {
updateFn(_activeFnIdx);
}
/* Отрисовать KaTeX на чипах/клавиатуре + следить за активным полем (идемпотентно). */
/* KaTeX на чипах/клавиатуре + math-поля + слежение за активным полем (идемпотентно). */
function _initGraphPanel() {
const root = document.getElementById('sim-graph');
if (!root || typeof katex === 'undefined') return;
root.querySelectorAll('.preset-btn[data-tex], .kp-btn[data-tex]').forEach(b => {
if (b.dataset.rendered) return;
try { b.innerHTML = katex.renderToString(b.dataset.tex, { throwOnError: false, strict: false, displayMode: false }); b.dataset.rendered = '1'; } catch (_) {}
if (_katexInto(b, b.dataset.tex)) b.dataset.rendered = '1';
});
if (!_graphPanelInit) {
_graphPanelInit = true;
[0, 1, 2].forEach(i => { const el = document.getElementById('fn' + i); if (el) el.addEventListener('focus', () => { _activeFnIdx = i; }); });
[0, 1, 2].forEach(i => {
const el = document.getElementById('fn' + i);
const m = document.getElementById('fn' + i + '-math');
if (el) {
el.addEventListener('focus', () => { _activeFnIdx = i; _fnDisplay(i); });
el.addEventListener('blur', () => { _fnDisplay(i); });
}
// клик по формуле → правка текста на месте
if (m) m.addEventListener('mousedown', (ev) => { ev.preventDefault(); const f = m.closest('.fn-field'); if (f) f.classList.remove('has-math'); el && el.focus(); });
});
}
[0, 1, 2].forEach(renderPreview);
[0, 1, 2].forEach(i => { renderPreview(i); _fnDisplay(i); });
}
/* debounced formula update */
@@ -661,6 +690,8 @@ class GraphSim {
document.getElementById('fn' + i).value = '';
document.getElementById('fn' + i + '-prev').innerHTML = '';
document.getElementById('fn' + i + '-prev').classList.remove('has-content');
const m = document.getElementById('fn' + i + '-math'); if (m) m.innerHTML = '';
const f = document.getElementById('fn' + i)?.closest('.fn-field'); if (f) f.classList.remove('has-math');
document.getElementById('fn' + i + '-err').classList.remove('show');
if (gSim) gSim.setFn(i, '', FN_COLORS[i]);
}