Fix diagram navigation and token highlighting
This commit is contained in:
1 parent
7ed885f0d1
commit
7aa729d41e
6 files changed
+354
-63
No files matched your search
+68
-6
@@ -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); };
|
||||
|
||||
Reference in new issue
Block a user