Fix diagram navigation and token highlighting

This commit is contained in:
alejo committed 2026-10-07 23:09:15 -03:00
1 parent 7ed885f0d1
commit 7aa729d41e
6 files changed
+354 -63

No files matched your search

+68 -6
View File
@@ -411,16 +411,78 @@ function savePng(data) {
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgMarkup(data));
}
/* Zoom speed. A mouse-wheel notch reports a deltaY of about 100 (a trackpad many
* small ones, a pinch gesture arrives as wheel + ctrlKey); each event changes the
* zoom by exp(-delta * rate), and no single event may change it by more than maxStep. */
var ZOOM = { wheel: 0.0006, pinch: 0.005, maxStep: 0.08, button: 1.2 };
function stopGlide() { $('stage').style.transition = ''; }
// Pans (and, if it is very small, zooms) so that the node is in the middle.
function focusNode(g) {
var c = $('dgm'), stage = $('stage'), cw = c.clientWidth, ch = c.clientHeight;
if (!cw || !ch || !g.getBBox) return;
var bb = g.getBBox();
var z = V.z < 0.9 ? 1 : V.z;
Array.prototype.forEach.call(stage.querySelectorAll('.al-node.focus'), function (n) { n.classList.remove('focus'); });
g.classList.add('focus');
V.z = z;
V.x = cw / 2 - (bb.x + bb.width / 2) * z;
V.y = ch / 2 - (bb.y + bb.height / 2) * z;
V.userMoved = true;
var calm = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
stage.style.transition = calm ? '' : 'transform 220ms ease';
applyView();
if (!calm) setTimeout(stopGlide, 260);
}
// The node that best matches the characters [a, b] (code point indices): the
// smallest one that contains them; a token wins over a rule of the same size.
function nodeAround(a, b) {
var list = $('stage').querySelectorAll('.al-node[data-s]'), best = null, span = Infinity, leaf = false;
for (var i = 0; i < list.length; i++) {
var g = list[i], s = +g.getAttribute('data-s'), e = +g.getAttribute('data-e');
if (s > a || e < b) continue;
var isLeaf = g.classList.contains('al-term') || g.classList.contains('al-error');
if (e - s < span || (e - s === span && !leaf)) { best = g; span = e - s; leaf = isLeaf; }
}
return best;
}
// A click (or a selection) in the input text: show where it is in the tree.
function focusDiagramAt(tab) {
var it = currentInput(), r = it ? S.results[it.id] : null;
if (!it || it !== tab || !r || r.rev !== it.rev) return; // not the parsed input, or its tree is out of date
if ($('p-diagram').hidden || $('dgm').hidden || !V.w) return; // the diagram is not on screen
var ta = it.ed.ta, text = ta.value;
var a = unitToCp(text, ta.selectionStart), b = unitToCp(text, ta.selectionEnd);
var g = null;
if (b > a) g = nodeAround(a, b - 1);
else g = nodeAround(a, a) || (a > 0 ? nodeAround(a - 1, a - 1) : null); // at the end of a token or of the text: the one before
if (g) focusNode(g);
}
function initDiagram() {
var box = $('dgm'), stage = $('stage'), drag = null, moved = false;
box.addEventListener('wheel', function (ev) {
if (!V.w) return;
ev.preventDefault();
var r = box.getBoundingClientRect(), unit = ev.deltaMode === 1 ? 16 : 1;
zoomAt(ev.clientX - r.left, ev.clientY - r.top, Math.exp(-ev.deltaY * unit * (ev.ctrlKey ? 0.01 : 0.0015)));
stopGlide();
var r = box.getBoundingClientRect(), unit = ev.deltaMode === 1 ? 16 : ev.deltaMode === 2 ? r.height : 1;
var step = clamp(-ev.deltaY * unit * (ev.ctrlKey ? ZOOM.pinch : ZOOM.wheel), -ZOOM.maxStep, ZOOM.maxStep);
zoomAt(ev.clientX - r.left, ev.clientY - r.top, Math.exp(step));
}, { passive: false });
// Dragging must pan, never select text; the diagram still has to get keyboard focus.
box.addEventListener('mousedown', function (ev) {
if (ev.button !== 0) return;
ev.preventDefault();
box.focus({ preventScroll: true });
});
box.addEventListener('selectstart', function (ev) { ev.preventDefault(); });
box.addEventListener('dragstart', function (ev) { ev.preventDefault(); });
box.addEventListener('pointerdown', function (ev) {
if (ev.button !== 0 || !V.w) return;
stopGlide();
drag = { x: ev.clientX, y: ev.clientY, vx: V.x, vy: V.y };
moved = false;
box.setPointerCapture(ev.pointerId);
@@ -451,8 +513,8 @@ function initDiagram() {
stage.addEventListener('mouseout', function (ev) { if (ev.target.closest('.al-node')) setRange('hover', -1, -1); });
box.addEventListener('keydown', function (ev) {
var k = ev.key;
if (k === '+' || k === '=') zoomCentre(1.25);
else if (k === '-') zoomCentre(0.8);
if (k === '+' || k === '=') zoomCentre(ZOOM.button);
else if (k === '-') zoomCentre(1 / ZOOM.button);
else if (k === '0') fit();
else if (k === 'ArrowLeft') { V.x += 60; V.userMoved = true; applyView(); }
else if (k === 'ArrowRight') { V.x -= 60; V.userMoved = true; applyView(); }
@@ -461,8 +523,8 @@ function initDiagram() {
else return;
ev.preventDefault();
});
$('zin').onclick = function () { zoomCentre(1.25); };
$('zout').onclick = function () { zoomCentre(0.8); };
$('zin').onclick = function () { zoomCentre(ZOOM.button); };
$('zout').onclick = function () { zoomCentre(1 / ZOOM.button); };
$('zfit').onclick = fit;
$('psvg').onclick = function () { var r = currentResult(); if (r && V.w) download(exportName('svg'), new Blob([svgMarkup(r.data)], { type: 'image/svg+xml' })); };
$('ppng').onclick = function () { var r = currentResult(); if (r && V.w) savePng(r.data); };