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:
+13
-7
@@ -181,22 +181,28 @@
|
|||||||
background: var(--fn-color, var(--violet));
|
background: var(--fn-color, var(--violet));
|
||||||
box-shadow: 0 0 6px var(--fn-color, var(--violet));
|
box-shadow: 0 0 6px var(--fn-color, var(--violet));
|
||||||
}
|
}
|
||||||
|
.fn-field { position: relative; flex: 1; min-width: 0; display: flex; align-items: center; }
|
||||||
.fn-input {
|
.fn-input {
|
||||||
flex: 1; border: none; outline: none; background: transparent;
|
width: 100%; border: none; outline: none; background: transparent;
|
||||||
font-family: 'Manrope', monospace; font-size: 0.88rem; font-weight: 600;
|
font-family: 'Manrope', monospace; font-size: 0.88rem; font-weight: 600;
|
||||||
color: var(--text); padding: 0; min-width: 0;
|
color: var(--text); padding: 0; min-width: 0;
|
||||||
}
|
}
|
||||||
.fn-input::placeholder { color: var(--text-3); font-weight: 500; }
|
.fn-input::placeholder { color: var(--text-3); font-weight: 500; }
|
||||||
|
|
||||||
/* KaTeX live preview — крупная «отрисованная» формула под вводом */
|
/* введённая функция как KaTeX прямо в строке; клик — правка текста на месте */
|
||||||
|
.fn-math { display: none; width: 100%; cursor: text; overflow-x: auto; overflow-y: hidden; min-height: 19px; line-height: 1.3; }
|
||||||
|
.fn-math .katex { color: var(--fn-color, rgba(255,255,255,.9)); font-size: 1.12em; }
|
||||||
|
.fn-field.has-math .fn-input { display: none; }
|
||||||
|
.fn-field.has-math .fn-math { display: block; }
|
||||||
|
|
||||||
|
/* живое превью формулы — только пока строка в режиме правки (поле в фокусе) */
|
||||||
.fn-preview {
|
.fn-preview {
|
||||||
min-height: 22px; padding: 5px 8px 5px 36px; margin-top: 3px;
|
min-height: 0; padding: 4px 8px 2px 36px; margin-top: 2px;
|
||||||
font-size: 0.82rem; line-height: 1.5;
|
font-size: 0.8rem; line-height: 1.4;
|
||||||
overflow-x: auto; overflow-y: hidden; display: none;
|
overflow-x: auto; overflow-y: hidden; display: none;
|
||||||
}
|
}
|
||||||
.fn-preview.has-content { display: block; }
|
.fn-row:focus-within + .fn-preview.has-content { display: block; }
|
||||||
.fn-preview .katex { color: var(--fn-color, rgba(255,255,255,.85)); font-size: 1.18em; }
|
.fn-preview .katex { color: var(--fn-color, rgba(255,255,255,.6)); font-size: 1.05em; opacity: .8; }
|
||||||
.fn-preview.is-ph { opacity: .4; } /* плейсхолдер-формула, пока поле пустое */
|
|
||||||
|
|
||||||
/* keypad вставки структур (как редактор формул) */
|
/* keypad вставки структур (как редактор формул) */
|
||||||
.gp-keypad { display: grid; grid-template-columns: repeat(6, 1fr); gap: 5px; margin-bottom: 6px; }
|
.gp-keypad { display: grid; grid-template-columns: repeat(6, 1fr); gap: 5px; margin-bottom: 6px; }
|
||||||
|
|||||||
+47
-16
@@ -519,6 +519,7 @@ class GraphSim {
|
|||||||
const el = document.getElementById(`fn${i}`);
|
const el = document.getElementById(`fn${i}`);
|
||||||
if (el) { el.value = fn.expr; }
|
if (el) { el.value = fn.expr; }
|
||||||
if (gSim) gSim.setFn(i, fn.expr, FN_COLORS[i]);
|
if (gSim) gSim.setFn(i, fn.expr, FN_COLORS[i]);
|
||||||
|
_fnDisplay(i);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -531,7 +532,7 @@ class GraphSim {
|
|||||||
gSim.onHover = updateInfoBar;
|
gSim.onHover = updateInfoBar;
|
||||||
if (!document.getElementById('fn0').value.trim()) {
|
if (!document.getElementById('fn0').value.trim()) {
|
||||||
document.getElementById('fn0').value = 'sin(x)';
|
document.getElementById('fn0').value = 'sin(x)';
|
||||||
renderPreview(0);
|
renderPreview(0); _fnDisplay(0);
|
||||||
gSim.fit();
|
gSim.fit();
|
||||||
gSim.setFn(0, 'sin(x)', FN_COLORS[0]);
|
gSim.setFn(0, 'sin(x)', FN_COLORS[0]);
|
||||||
return;
|
return;
|
||||||
@@ -574,22 +575,40 @@ class GraphSim {
|
|||||||
.replace(/\*/g, '\\cdot ');
|
.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) {
|
function renderPreview(idx) {
|
||||||
const inp = document.getElementById('fn' + idx);
|
const inp = document.getElementById('fn' + idx);
|
||||||
const prev = document.getElementById('fn' + idx + '-prev');
|
const prev = document.getElementById('fn' + idx + '-prev');
|
||||||
if (!prev) return;
|
if (!prev) return;
|
||||||
const raw = inp?.value?.trim() || '';
|
const raw = inp?.value?.trim() || '';
|
||||||
const src = raw || (inp?.getAttribute('placeholder') || ''); // пусто → показываем плейсхолдер-формулу
|
if (!raw || typeof katex === 'undefined') { prev.innerHTML = ''; prev.classList.remove('has-content'); return; }
|
||||||
if (!src || typeof katex === 'undefined') {
|
if (_katexInto(prev, toLatex(raw))) prev.classList.add('has-content');
|
||||||
prev.innerHTML = ''; prev.classList.remove('has-content', 'is-ph'); return;
|
else prev.classList.remove('has-content');
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
prev.innerHTML = katex.renderToString(toLatex(src), {
|
/* Введённая функция — отрисованной формулой KaTeX прямо в строке. */
|
||||||
throwOnError: false, strict: false, displayMode: false,
|
function renderFnMath(idx) {
|
||||||
});
|
const inp = document.getElementById('fn' + idx);
|
||||||
prev.classList.add('has-content');
|
const m = document.getElementById('fn' + idx + '-math');
|
||||||
prev.classList.toggle('is-ph', !raw); // плейсхолдер — приглушённо
|
if (!m || !inp) return;
|
||||||
} catch { prev.innerHTML = ''; prev.classList.remove('has-content', 'is-ph'); }
|
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) {
|
function graphInsert(token) {
|
||||||
const el = document.getElementById('fn' + _activeFnIdx) || document.getElementById('fn0');
|
const el = document.getElementById('fn' + _activeFnIdx) || document.getElementById('fn0');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
const f = el.closest('.fn-field'); if (f) f.classList.remove('has-math'); // в режим правки
|
||||||
el.focus();
|
el.focus();
|
||||||
let ins = String(token || ''); let caretInTok = -1;
|
let ins = String(token || ''); let caretInTok = -1;
|
||||||
const bar = ins.indexOf('|');
|
const bar = ins.indexOf('|');
|
||||||
@@ -610,19 +630,28 @@ class GraphSim {
|
|||||||
updateFn(_activeFnIdx);
|
updateFn(_activeFnIdx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Отрисовать KaTeX на чипах/клавиатуре + следить за активным полем (идемпотентно). */
|
/* KaTeX на чипах/клавиатуре + math-поля + слежение за активным полем (идемпотентно). */
|
||||||
function _initGraphPanel() {
|
function _initGraphPanel() {
|
||||||
const root = document.getElementById('sim-graph');
|
const root = document.getElementById('sim-graph');
|
||||||
if (!root || typeof katex === 'undefined') return;
|
if (!root || typeof katex === 'undefined') return;
|
||||||
root.querySelectorAll('.preset-btn[data-tex], .kp-btn[data-tex]').forEach(b => {
|
root.querySelectorAll('.preset-btn[data-tex], .kp-btn[data-tex]').forEach(b => {
|
||||||
if (b.dataset.rendered) return;
|
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) {
|
if (!_graphPanelInit) {
|
||||||
_graphPanelInit = true;
|
_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 */
|
/* debounced formula update */
|
||||||
@@ -661,6 +690,8 @@ class GraphSim {
|
|||||||
document.getElementById('fn' + i).value = '';
|
document.getElementById('fn' + i).value = '';
|
||||||
document.getElementById('fn' + i + '-prev').innerHTML = '';
|
document.getElementById('fn' + i + '-prev').innerHTML = '';
|
||||||
document.getElementById('fn' + i + '-prev').classList.remove('has-content');
|
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');
|
document.getElementById('fn' + i + '-err').classList.remove('show');
|
||||||
if (gSim) gSim.setFn(i, '', FN_COLORS[i]);
|
if (gSim) gSim.setFn(i, '', FN_COLORS[i]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
<div class="fn-row">
|
<div class="fn-row">
|
||||||
<div class="fn-dot"></div>
|
<div class="fn-dot"></div>
|
||||||
<span class="fn-label">y =</span>
|
<span class="fn-label">y =</span>
|
||||||
<input class="fn-input" id="fn0" placeholder="sin(x)" autocomplete="off" spellcheck="false" oninput="updateFn(0)" />
|
<div class="fn-field">
|
||||||
|
<input class="fn-input" id="fn0" placeholder="sin(x)" autocomplete="off" spellcheck="false" oninput="updateFn(0)" />
|
||||||
|
<div class="fn-math" id="fn0-math" title="Нажми, чтобы изменить"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fn-preview" id="fn0-prev"></div>
|
<div class="fn-preview" id="fn0-prev"></div>
|
||||||
<div class="fn-err" id="fn0-err">Синтаксическая ошибка</div>
|
<div class="fn-err" id="fn0-err">Синтаксическая ошибка</div>
|
||||||
@@ -21,7 +24,10 @@
|
|||||||
<div class="fn-row">
|
<div class="fn-row">
|
||||||
<div class="fn-dot"></div>
|
<div class="fn-dot"></div>
|
||||||
<span class="fn-label">y =</span>
|
<span class="fn-label">y =</span>
|
||||||
<input class="fn-input" id="fn1" placeholder="x^2 - 4" autocomplete="off" spellcheck="false" oninput="updateFn(1)" />
|
<div class="fn-field">
|
||||||
|
<input class="fn-input" id="fn1" placeholder="x^2 - 4" autocomplete="off" spellcheck="false" oninput="updateFn(1)" />
|
||||||
|
<div class="fn-math" id="fn1-math" title="Нажми, чтобы изменить"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fn-preview" id="fn1-prev"></div>
|
<div class="fn-preview" id="fn1-prev"></div>
|
||||||
<div class="fn-err" id="fn1-err">Синтаксическая ошибка</div>
|
<div class="fn-err" id="fn1-err">Синтаксическая ошибка</div>
|
||||||
@@ -32,7 +38,10 @@
|
|||||||
<div class="fn-row">
|
<div class="fn-row">
|
||||||
<div class="fn-dot"></div>
|
<div class="fn-dot"></div>
|
||||||
<span class="fn-label">y =</span>
|
<span class="fn-label">y =</span>
|
||||||
<input class="fn-input" id="fn2" placeholder="tg(x)" autocomplete="off" spellcheck="false" oninput="updateFn(2)" />
|
<div class="fn-field">
|
||||||
|
<input class="fn-input" id="fn2" placeholder="tg(x)" autocomplete="off" spellcheck="false" oninput="updateFn(2)" />
|
||||||
|
<div class="fn-math" id="fn2-math" title="Нажми, чтобы изменить"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fn-preview" id="fn2-prev"></div>
|
<div class="fn-preview" id="fn2-prev"></div>
|
||||||
<div class="fn-err" id="fn2-err">Синтаксическая ошибка</div>
|
<div class="fn-err" id="fn2-err">Синтаксическая ошибка</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user