227 lines
9.2 KiB
JavaScript
227 lines
9.2 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
/* ================================================================== helpers */
|
|
var $ = function (id) { return document.getElementById(id); };
|
|
function el(tag, cls, text) {
|
|
var e = document.createElement(tag);
|
|
if (cls) e.className = cls;
|
|
if (text !== undefined && text !== null) e.textContent = text;
|
|
return e;
|
|
}
|
|
function clear(n) { while (n.firstChild) n.removeChild(n.firstChild); }
|
|
function plural(n, w) { return n + ' ' + w + (n === 1 ? '' : 's'); }
|
|
function basename(p) { var i = p.lastIndexOf('/'); return i < 0 ? p : p.slice(i + 1); }
|
|
function dirname(p) { var i = p.lastIndexOf('/'); return i < 0 ? '' : p.slice(0, i); }
|
|
function joinPath(d, n) { return d ? d + '/' + n : n; }
|
|
function stem(n) { var i = n.lastIndexOf('.'); return i <= 0 ? n : n.slice(0, i); }
|
|
function isGrammarFile(p) { return /\.g4$/i.test(p); }
|
|
function human(n) { return n < 1024 ? n + ' B' : n < 1048576 ? (n / 1024).toFixed(1) + ' KB' : (n / 1048576).toFixed(1) + ' MB'; }
|
|
|
|
var store = {
|
|
get: function (k, d) {
|
|
try { var v = localStorage.getItem('antlrlab:' + k); return v === null ? d : JSON.parse(v); } catch (e) { return d; }
|
|
},
|
|
set: function (k, v) { try { localStorage.setItem('antlrlab:' + k, JSON.stringify(v)); } catch (e) { /* ignore */ } }
|
|
};
|
|
|
|
function debounce(fn, ms) {
|
|
var t = null;
|
|
var d = function () { clearTimeout(t); var a = arguments; t = setTimeout(function () { fn.apply(null, a); }, ms); };
|
|
d.cancel = function () { clearTimeout(t); };
|
|
return d;
|
|
}
|
|
|
|
/* ==================================================================== state */
|
|
var S = {
|
|
info: null,
|
|
tabs: [],
|
|
active: null, // id of the visible tab
|
|
lastGrammar: null, // the grammar that Run uses
|
|
lastInput: null, // the input that Run uses
|
|
results: {}, // input tab id -> { data, rev, grev }
|
|
gres: null, // last grammar outcome from the server
|
|
crash: null, // set when the last run crashed, timed out or failed in the worker
|
|
grev: 0, // bumps on every grammar edit
|
|
start: {}, // grammar name -> chosen start rule
|
|
prefs: { autorun: true, theme: 'auto', fs: 13, diagram: true, output: true, view: 'diagram', out: 'problems' },
|
|
running: false,
|
|
queued: false,
|
|
untitled: { grammar: 0, input: 0 }
|
|
};
|
|
|
|
/* ====================================================================== API */
|
|
function api(method, path, body) {
|
|
var opts = { method: method, credentials: 'same-origin', headers: { 'X-Requested-With': 'antlrlab' } };
|
|
if (body !== undefined) {
|
|
opts.body = JSON.stringify(body);
|
|
opts.headers['Content-Type'] = 'application/json';
|
|
}
|
|
return fetch(path, opts).then(function (r) {
|
|
return r.text().then(function (t) {
|
|
var data = null;
|
|
try { data = JSON.parse(t); } catch (e) { /* not JSON */ }
|
|
if (!r.ok) {
|
|
var err = new Error((data && data.error) || ('HTTP ' + r.status));
|
|
err.status = r.status;
|
|
err.data = data;
|
|
throw err;
|
|
}
|
|
return data;
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ==================================================================== toast */
|
|
function toast(msg, kind) {
|
|
var t = el('div', 'toast' + (kind ? ' ' + kind : ''), msg);
|
|
$('toasts').appendChild(t);
|
|
setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, kind === 'error' ? 6500 : 2600);
|
|
}
|
|
|
|
/* ================================================================== dialogs */
|
|
// modal({title, build(ctx) -> Node, buttons: [{label, value, primary}]}) -> Promise(value | null)
|
|
function modal(opts) {
|
|
return new Promise(function (resolve) {
|
|
var root = $('modal-root');
|
|
var prev = document.activeElement;
|
|
clear(root);
|
|
var back = el('div', 'modal-back');
|
|
var box = el('div', 'modal');
|
|
box.setAttribute('role', 'dialog');
|
|
box.setAttribute('aria-modal', 'true');
|
|
box.appendChild(el('h3', '', opts.title));
|
|
var body = el('div', 'mbody');
|
|
var ctx = { close: close, box: box, accept: null };
|
|
if (opts.message) body.appendChild(el('p', '', opts.message));
|
|
if (opts.build) body.appendChild(opts.build(ctx));
|
|
box.appendChild(body);
|
|
var foot = el('div', 'mfoot');
|
|
var primary = null;
|
|
(opts.buttons || []).forEach(function (b) {
|
|
var btn = el('button', 'btn' + (b.primary ? ' primary' : ''), b.label);
|
|
btn.type = 'button';
|
|
btn.onclick = function () {
|
|
if (b.check && !b.check()) return;
|
|
close(b.value === undefined ? b.label : b.value);
|
|
};
|
|
if (b.primary) primary = btn;
|
|
foot.appendChild(btn);
|
|
});
|
|
box.appendChild(foot);
|
|
back.appendChild(box);
|
|
root.appendChild(back);
|
|
|
|
function onKey(ev) {
|
|
if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); close(null); }
|
|
else if (ev.key === 'Enter' && primary && ev.target.tagName !== 'BUTTON' && !(ev.target.closest && ev.target.closest('.flist'))) {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
primary.click();
|
|
}
|
|
}
|
|
back.addEventListener('mousedown', function (ev) { if (ev.target === back) close(null); });
|
|
document.addEventListener('keydown', onKey, true);
|
|
function close(v) {
|
|
document.removeEventListener('keydown', onKey, true);
|
|
clear(root);
|
|
if (prev && prev.focus) prev.focus();
|
|
resolve(v);
|
|
}
|
|
var first = box.querySelector('[data-focus]') || primary || box.querySelector('button');
|
|
if (first) first.focus();
|
|
});
|
|
}
|
|
|
|
function ask(title, message, buttons) { return modal({ title: title, message: message, buttons: buttons }); }
|
|
|
|
/* ===================================================================== menus */
|
|
var menus = []; // [{label, items: function() -> [...]}]
|
|
var openMenu = -1;
|
|
var dropdown = null;
|
|
|
|
function defineMenus(list) {
|
|
menus = list;
|
|
var bar = $('menubar');
|
|
clear(bar);
|
|
menus.forEach(function (m, i) {
|
|
var b = el('button', 'menu-btn', m.label);
|
|
b.type = 'button';
|
|
b.setAttribute('role', 'menuitem');
|
|
b.setAttribute('aria-haspopup', 'true');
|
|
b.setAttribute('aria-expanded', 'false');
|
|
b.onclick = function () { if (openMenu === i) closeMenu(); else showMenu(i, false); };
|
|
b.onmouseenter = function () { if (openMenu >= 0 && openMenu !== i) showMenu(i, false); };
|
|
b.onkeydown = function (ev) {
|
|
if (ev.key === 'ArrowDown' || ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); showMenu(i, true); }
|
|
else if (ev.key === 'ArrowRight') { ev.preventDefault(); bar.children[(i + 1) % menus.length].focus(); }
|
|
else if (ev.key === 'ArrowLeft') { ev.preventDefault(); bar.children[(i + menus.length - 1) % menus.length].focus(); }
|
|
};
|
|
bar.appendChild(b);
|
|
});
|
|
var rn = el('span', '', '');
|
|
rn.id = 'rootname';
|
|
bar.appendChild(rn);
|
|
}
|
|
|
|
function closeMenu(refocus) {
|
|
if (dropdown) { dropdown.remove(); dropdown = null; }
|
|
var bar = $('menubar');
|
|
Array.prototype.forEach.call(bar.querySelectorAll('.menu-btn'), function (b) { b.setAttribute('aria-expanded', 'false'); });
|
|
var was = openMenu;
|
|
openMenu = -1;
|
|
if (refocus && was >= 0) bar.children[was].focus();
|
|
}
|
|
|
|
function showMenu(i, focusFirst) {
|
|
closeMenu();
|
|
openMenu = i;
|
|
var btn = $('menubar').children[i];
|
|
btn.setAttribute('aria-expanded', 'true');
|
|
var dd = el('div', 'dropdown');
|
|
dd.setAttribute('role', 'menu');
|
|
menus[i].items().forEach(function (it) {
|
|
if (it.sep) { dd.appendChild(el('div', 'sep')); return; }
|
|
var row = el('div', 'mi');
|
|
row.setAttribute('role', 'menuitem');
|
|
row.tabIndex = -1;
|
|
if (it.disabled) row.setAttribute('aria-disabled', 'true');
|
|
if (it.checked) { row.appendChild(el('span', 'ck', '\u2713')); row.setAttribute('aria-checked', 'true'); }
|
|
row.appendChild(el('span', '', it.label));
|
|
if (it.key) row.appendChild(el('span', 'k', it.key));
|
|
var run = function () { closeMenu(); setTimeout(it.action, 0); };
|
|
row.onclick = run;
|
|
row.onkeydown = function (ev) {
|
|
var items = Array.prototype.filter.call(dd.children, function (c) { return c.classList.contains('mi') && c.getAttribute('aria-disabled') !== 'true'; });
|
|
var at = items.indexOf(row);
|
|
if (ev.key === 'ArrowDown') { ev.preventDefault(); items[(at + 1) % items.length].focus(); }
|
|
else if (ev.key === 'ArrowUp') { ev.preventDefault(); items[(at + items.length - 1) % items.length].focus(); }
|
|
else if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); run(); }
|
|
else if (ev.key === 'Escape') { ev.preventDefault(); closeMenu(true); }
|
|
else if (ev.key === 'ArrowRight' || ev.key === 'ArrowLeft') {
|
|
ev.preventDefault();
|
|
var n = (i + (ev.key === 'ArrowRight' ? 1 : menus.length - 1)) % menus.length;
|
|
showMenu(n, true);
|
|
$('menubar').children[n].focus();
|
|
var first = dropdown && dropdown.querySelector('.mi:not([aria-disabled=true])');
|
|
if (first) first.focus();
|
|
}
|
|
};
|
|
dd.appendChild(row);
|
|
});
|
|
document.body.appendChild(dd);
|
|
var r = btn.getBoundingClientRect();
|
|
dd.style.left = Math.max(0, Math.min(r.left, window.innerWidth - dd.offsetWidth - 4)) + 'px';
|
|
dd.style.top = r.bottom + 'px';
|
|
dropdown = dd;
|
|
if (focusFirst) {
|
|
var f = dd.querySelector('.mi:not([aria-disabled=true])');
|
|
if (f) f.focus();
|
|
}
|
|
}
|
|
|
|
document.addEventListener('mousedown', function (ev) {
|
|
if (dropdown && !dropdown.contains(ev.target) && !ev.target.closest('.menu-btn')) closeMenu();
|
|
});
|
|
window.addEventListener('blur', function () { closeMenu(); });
|