Files
lpra/internal/server/web/static/dbedit.js
T

327 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SQLite editor of the file manager: the agent opens the database and runs
// the queries, so a database in use keeps its locks. Tables and views in
// tabs, rows a page at a time; a click on a value edits it (Enter saves,
// Esc cancels, Shift+Enter a new line, the ∅ button makes it NULL). Rows are
// added and deleted, and a tab runs any SQL.
(function () {
'use strict';
var PAGE = 200;
var esc = function (s) { return String(s).replace(/[&<>"]/g, function (c) { return {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'}[c]; }); };
function cellHTML(c) {
if (c.t === 'n') return '<i class="dbx-null">NULL</i>';
if (c.t === 'b') return '<i class="dbx-null">BLOB ' + esc(c.v) + ' Б</i>';
var v = c.v || '';
return esc(v.length > 300 ? v.slice(0, 300) + '…' : v);
}
// The type a typed value goes as: what the value was, or what the column
// is declared as, when the text fits it.
function typeFor(text, was, decl) {
var d = (decl || '').toUpperCase();
var int = /^\s*-?\d+\s*$/.test(text), num = text.trim() !== '' && isFinite(Number(text));
if (was === 'i' || (was !== 's' && /INT/.test(d))) return int ? 'i' : num ? 'r' : 's';
if (was === 'r' || (was !== 's' && /REAL|FLOA|DOUB|NUM|DEC/.test(d))) return num ? 'r' : 's';
return 's';
}
function open(box, path, call, toast) {
var S = {tables: [], cur: null, offset: 0, data: null, sel: new Set()};
box.innerHTML =
'<div class="dbx">' +
'<div class="dbx-tabs" role="tablist"></div>' +
'<div class="dbx-tools">' +
' <span class="muted dbx-info"></span>' +
' <span class="dbx-pager"><button type="button" class="btn ghost small icon dbx-prev" title="Назад" aria-label="Назад">‹</button>' +
' <span class="muted dbx-range"></span><button type="button" class="btn ghost small icon dbx-next" title="Дальше" aria-label="Дальше">›</button></span>' +
' <span class="dbx-sp"></span>' +
' <button type="button" class="btn ghost small dbx-add">+ Строка</button>' +
' <button type="button" class="btn danger small dbx-del" disabled>Удалить</button>' +
'</div>' +
'<div class="dbx-grid"></div>' +
'<div class="dbx-sql" hidden>' +
' <textarea class="dbx-q" spellcheck="false" placeholder="SELECT * FROM … - Ctrl+Enter выполнить"></textarea>' +
' <div class="dbx-sqlbar"><button type="button" class="dbx-run">Выполнить</button><span class="muted dbx-sqlmsg"></span></div>' +
' <div class="dbx-sqlres"></div>' +
'</div>' +
'</div>';
var $ = function (s) { return box.querySelector(s); };
function busy(on) { box.querySelector('.dbx').classList.toggle('busy', on); }
function req(op, args) {
args.path = path;
busy(true);
return call(op, args).then(function (r) { busy(false); return r; }, function (e) { busy(false); throw e; });
}
function tabs() {
var t = $('.dbx-tabs');
t.innerHTML = '';
S.tables.forEach(function (x) {
var b = document.createElement('button');
b.type = 'button';
b.textContent = x.name;
if (x.type === 'view') b.title = 'представление - только чтение';
b.className = x.name === S.cur ? 'on' : '';
b.onclick = function () { show(x.name, 0); };
t.appendChild(b);
});
var q = document.createElement('button');
q.type = 'button';
q.textContent = 'SQL';
q.className = 'dbx-sqltab' + (S.cur === null ? ' on' : '');
q.onclick = sqlTab;
t.appendChild(q);
}
function show(name, offset) {
S.cur = name; S.offset = offset; S.sel.clear();
tabs();
$('.dbx-sql').hidden = true;
$('.dbx-grid').hidden = false;
$('.dbx-tools').hidden = false;
return req('db.rows', {table: name, offset: offset, limit: PAGE}).then(function (r) {
if (!r.ok) { $('.dbx-grid').innerHTML = '<p class="dv-note">' + esc(r.error || 'ошибка') + '</p>'; return; }
S.data = r.data;
draw();
});
}
function draw() {
var d = S.data, t = d.table, editable = !!t.key;
// An INTEGER PRIMARY KEY column already shows the rowid.
var showRowid = t.key === 'rowid' && !t.alias;
$('.dbx-info').textContent = d.total + ' ' + plural(d.total) + (t.type === 'view' ? ' · представление' : !editable ? ' · только чтение' : '');
$('.dbx-range').textContent = d.rows.length ? (d.offset + 1) + '–' + (d.offset + d.rows.length) : '';
$('.dbx-prev').disabled = d.offset <= 0;
$('.dbx-next').disabled = d.offset + d.rows.length >= d.total;
$('.dbx-pager').hidden = d.total <= PAGE;
$('.dbx-add').hidden = t.type !== 'table';
$('.dbx-del').hidden = !editable;
selInfo();
var h = '<table class="dbx-t"><thead><tr>' + (editable ? '<th class="dbx-ck"><input type="checkbox" class="dbx-all" aria-label="Выбрать все"></th>' : '') +
(showRowid ? '<th class="dbx-k">rowid</th>' : '');
t.columns.forEach(function (c) {
h += '<th title="' + esc(c.type + (c.pk ? ' · ключ' : '') + (c.notnull ? ' · NOT NULL' : '')) + '">' + esc(c.name) +
(c.type ? '<small>' + esc(c.type.toLowerCase()) + '</small>' : '') + '</th>';
});
h += '</tr></thead><tbody>';
d.rows.forEach(function (row, i) {
h += '<tr data-i="' + i + '">' + (editable ? '<td class="dbx-ck"><input type="checkbox" aria-label="Выбрать строку"></td>' : '') +
(showRowid ? '<td class="dbx-k">' + esc(row.k) + '</td>' : '');
row.v.forEach(function (c, j) {
h += '<td data-j="' + j + '" class="' + (c.t === 'i' || c.t === 'r' ? 'num' : '') + (editable && c.t !== 'b' ? ' ed' : '') + '">' + cellHTML(c) + '</td>';
});
h += '</tr>';
});
h += '</tbody></table>';
if (!d.rows.length) h += '<p class="dv-note">строк нет</p>';
$('.dbx-grid').innerHTML = h;
$('.dbx-grid').scrollTop = 0;
fixWidths($('.dbx-grid table'));
}
// The columns keep the widths they first came out at, so a value being
// edited does not move the table about.
function fixWidths(table) {
if (!table) return;
var ths = table.querySelectorAll('thead th'), ws = [], sum = 0;
ths.forEach(function (th) { var w = Math.ceil(th.getBoundingClientRect().width); ws.push(w); sum += w; });
ths.forEach(function (th, i) { th.style.width = ws[i] + 'px'; });
table.style.width = sum + 'px';
table.style.tableLayout = 'fixed';
}
function plural(n) {
var m10 = n % 10, m100 = n % 100;
return m10 === 1 && m100 !== 11 ? 'строка' : m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14) ? 'строки' : 'строк';
}
function selInfo() {
var b = $('.dbx-del');
b.disabled = !S.sel.size;
b.textContent = S.sel.size ? 'Удалить (' + S.sel.size + ')' : 'Удалить';
}
$('.dbx-prev').onclick = function () { show(S.cur, Math.max(0, S.offset - PAGE)); };
$('.dbx-next').onclick = function () { show(S.cur, S.offset + PAGE); };
// ---- selecting rows ----
$('.dbx-grid').addEventListener('change', function (ev) {
var el = ev.target;
if (el.classList.contains('dbx-all')) {
box.querySelectorAll('tbody .dbx-ck input').forEach(function (c, i) { c.checked = el.checked; el.checked ? S.sel.add(i) : S.sel.delete(i); });
} else if (el.closest('tbody .dbx-ck')) {
var i = +el.closest('tr').dataset.i;
el.checked ? S.sel.add(i) : S.sel.delete(i);
}
selInfo();
});
// ---- editing a value ----
var editing = null;
$('.dbx-grid').addEventListener('click', function (ev) {
var td = ev.target.closest('td.ed');
if (!td || td.querySelector('textarea') || ev.target.closest('.dbx-edit')) return;
edit(td);
});
function edit(td) {
if (editing) finish(editing, true);
var i = +td.parentNode.dataset.i, j = +td.dataset.j;
var row = S.data.rows[i], cell = row.v[j], col = S.data.table.columns[j];
var hgt = td.offsetHeight;
td.classList.add('editing');
td.innerHTML = '<div class="dbx-edit"><textarea spellcheck="false"></textarea><button type="button" class="dbx-nullb" title="Сделать NULL" aria-label="Сделать NULL">∅</button></div>';
var ta = td.querySelector('textarea');
ta.value = cell.t === 'n' ? '' : cell.v;
ta.placeholder = cell.t === 'n' ? 'NULL' : '';
ta.style.height = Math.max(hgt, 30) + 'px';
ta.focus();
ta.select();
editing = {td: td, i: i, j: j, ta: ta, cell: cell, col: col, nul: false};
ta.addEventListener('keydown', function (ev) {
if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); finish(editing, true); }
else if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); finish(editing, false); }
else if (ev.key === 'Tab') {
ev.preventDefault();
var cur = editing, next = ev.shiftKey ? cur.td.previousElementSibling : cur.td.nextElementSibling;
finish(cur, true);
if (next && next.classList.contains('ed')) edit(next);
}
});
ta.addEventListener('blur', function () { setTimeout(function () { if (editing && editing.ta === ta && document.activeElement !== td.querySelector('.dbx-nullb')) finish(editing, true); }, 120); });
td.querySelector('.dbx-nullb').onmousedown = function (ev) { ev.preventDefault(); editing.nul = true; finish(editing, true); };
}
function finish(e, keep) {
if (!e || editing !== e) return;
editing = null;
var text = e.ta.value, cell = e.cell;
var value = e.nul ? {t: 'n'} : {t: typeFor(text, cell.t, e.col.type), v: text};
var same = e.nul ? cell.t === 'n' : (cell.t !== 'n' || text !== '') && text === (cell.v || '') && (cell.t === value.t || cell.t === 's' || value.t === 's');
if (cell.t === 'n' && !e.nul && text === '') same = true;
e.td.classList.remove('editing');
if (!keep || same) { e.td.innerHTML = cellHTML(cell); return; }
e.td.innerHTML = cellHTML(value);
e.td.classList.add('saving');
req('db.set', {table: S.cur, key: S.data.rows[e.i].k, column: e.col.name, value: value}).then(function (r) {
e.td.classList.remove('saving');
if (!r.ok) {
e.td.innerHTML = cellHTML(cell);
flash(e.td, 'bad');
toast(r.error || 'не сохранилось', true);
return;
}
var got = r.data.value;
S.data.rows[e.i].v[e.j] = got;
// A changed key column moves the row's key too.
if (r.data.key !== undefined) S.data.rows[e.i].k = r.data.key;
e.td.innerHTML = cellHTML(got);
e.td.className = (got.t === 'i' || got.t === 'r' ? 'num' : '') + ' ed';
flash(e.td, 'ok');
});
}
function flash(td, kind) {
td.classList.remove('flash-ok', 'flash-bad');
void td.offsetWidth;
td.classList.add('flash-' + kind);
}
// ---- adding and deleting rows ----
$('.dbx-add').onclick = function () {
if (box.querySelector('tr.dbx-new')) return;
var t = S.data.table, tbody = box.querySelector('.dbx-t tbody');
var tr = document.createElement('tr');
tr.className = 'dbx-new';
var h = (t.key ? '<td class="dbx-ck"></td>' : '') + (t.key === 'rowid' && !t.alias ? '<td class="dbx-k">новая</td>' : '');
t.columns.forEach(function (c) {
h += '<td><input type="text" data-c="' + esc(c.name) + '" placeholder="' + esc(c.default ? 'по умолч.: ' + c.default : c.notnull ? 'обязательно' : 'NULL') + '"></td>';
});
tr.innerHTML = h;
tbody.insertBefore(tr, tbody.firstChild);
var bar = document.createElement('tr');
bar.className = 'dbx-newbar';
bar.innerHTML = '<td colspan="' + (t.columns.length + 2) + '"><button type="button" class="small dbx-addok">Добавить</button> <button type="button" class="btn ghost small dbx-addno">Отмена</button> <span class="muted">пустое поле - значение по умолчанию</span></td>';
tbody.insertBefore(bar, tr.nextSibling);
$('.dbx-grid').scrollTop = 0;
tr.querySelector('input').focus();
var done = function () { tr.remove(); bar.remove(); };
bar.querySelector('.dbx-addno').onclick = done;
var add = function () {
var values = {};
tr.querySelectorAll('input').forEach(function (inp) {
if (inp.value === '') return;
var c = t.columns.filter(function (x) { return x.name === inp.dataset.c; })[0];
values[c.name] = {t: typeFor(inp.value, '', c.type), v: inp.value};
});
req('db.add', {table: S.cur, values: values}).then(function (r) {
if (!r.ok) { toast(r.error || 'не добавилось', true); return; }
toast('строка добавлена');
var total = S.data.total + 1;
show(S.cur, Math.floor((total - 1) / PAGE) * PAGE);
});
};
bar.querySelector('.dbx-addok').onclick = add;
tr.addEventListener('keydown', function (ev) {
if (ev.key === 'Enter') { ev.preventDefault(); add(); }
if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); done(); }
});
};
$('.dbx-del').onclick = function () {
var keys = [];
S.sel.forEach(function (i) { keys.push(S.data.rows[i].k); });
if (!keys.length || !confirm('Удалить строк: ' + keys.length + '?')) return;
req('db.del', {table: S.cur, keys: keys}).then(function (r) {
if (!r.ok) { toast(r.error || 'не удалилось', true); return; }
toast('удалено строк: ' + r.data.deleted);
show(S.cur, S.offset);
});
};
// ---- SQL ----
function sqlTab() {
S.cur = null;
tabs();
$('.dbx-grid').hidden = true;
$('.dbx-tools').hidden = true;
$('.dbx-sql').hidden = false;
$('.dbx-q').focus();
}
function run() {
var q = $('.dbx-q').value;
if (!q.trim()) return;
$('.dbx-sqlmsg').textContent = '';
req('db.sql', {sql: q}).then(function (r) {
var out = $('.dbx-sqlres');
if (!r.ok) { out.innerHTML = ''; $('.dbx-sqlmsg').textContent = r.error || 'ошибка'; $('.dbx-sqlmsg').className = 'dbx-sqlmsg bad'; return; }
$('.dbx-sqlmsg').className = 'muted dbx-sqlmsg';
var d = r.data;
if (d.columns) {
$('.dbx-sqlmsg').textContent = 'строк: ' + d.rows.length + (d.more ? ' (показаны первые 500)' : '');
var h = '<table class="dbx-t"><thead><tr>' + d.columns.map(function (c) { return '<th>' + esc(c) + '</th>'; }).join('') + '</tr></thead><tbody>';
d.rows.forEach(function (row) {
h += '<tr>' + row.map(function (c) { return '<td class="' + (c.t === 'i' || c.t === 'r' ? 'num' : '') + '">' + cellHTML(c) + '</td>'; }).join('') + '</tr>';
});
out.innerHTML = h + '</tbody></table>';
} else {
out.innerHTML = '';
$('.dbx-sqlmsg').textContent = 'готово · изменено строк: ' + d.changed;
loadTables(true);
}
});
}
$('.dbx-run').onclick = run;
$('.dbx-q').addEventListener('keydown', function (ev) {
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) { ev.preventDefault(); run(); }
});
function loadTables(keep) {
return req('db.tables', {}).then(function (r) {
if (!r.ok) return r;
S.tables = r.data.tables;
if (keep) { tabs(); return r; }
if (S.tables.length) show(S.tables[0].name, 0); else sqlTab();
return r;
});
}
return loadTables(false);
}
window.DBEdit = {open: open};
})();