// 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 {'&': '&', '<': '<', '>': '>', '"': '"'}[c]; }); }; function cellHTML(c) { if (c.t === 'n') return 'NULL'; if (c.t === 'b') return 'BLOB ' + esc(c.v) + ' Б'; 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 = '
' + '
' + '
' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '
' + '
' + '' + '
'; 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 = '

' + esc(r.error || 'ошибка') + '

'; 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 = '' + (editable ? '' : '') + (showRowid ? '' : ''); t.columns.forEach(function (c) { h += ''; }); h += ''; d.rows.forEach(function (row, i) { h += '' + (editable ? '' : '') + (showRowid ? '' : ''); row.v.forEach(function (c, j) { h += ''; }); h += ''; }); h += '
rowid' + esc(c.name) + (c.type ? '' + esc(c.type.toLowerCase()) + '' : '') + '
' + esc(row.k) + '' + cellHTML(c) + '
'; if (!d.rows.length) h += '

строк нет

'; $('.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 = '
'; 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 ? '' : '') + (t.key === 'rowid' && !t.alias ? 'новая' : ''); t.columns.forEach(function (c) { h += ''; }); tr.innerHTML = h; tbody.insertBefore(tr, tbody.firstChild); var bar = document.createElement('tr'); bar.className = 'dbx-newbar'; bar.innerHTML = ' пустое поле - значение по умолчанию'; 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 = '' + d.columns.map(function (c) { return ''; }).join('') + ''; d.rows.forEach(function (row) { h += '' + row.map(function (c) { return ''; }).join('') + ''; }); out.innerHTML = h + '
' + esc(c) + '
' + cellHTML(c) + '
'; } 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}; })();