mirror of
https://github.com/sqlite/sqlite.git
synced 2025-08-08 14:02:16 +03:00
299 lines
12 KiB
JavaScript
299 lines
12 KiB
JavaScript
/*
|
|
2022-05-20
|
|
|
|
The author disclaims copyright to this source code. In place of a
|
|
legal notice, here is a blessing:
|
|
|
|
* May you do good and not evil.
|
|
* May you find forgiveness for yourself and forgive others.
|
|
* May you share freely, never taking more than you give.
|
|
|
|
***********************************************************************
|
|
|
|
This is the --post-js file for emcc. It gets appended to the
|
|
generated fiddle.js. It should contain (or load) all app-level code.
|
|
|
|
Maintenance achtung: do not call any wasm-bound functions from
|
|
outside of the onRuntimeInitialized() function. They are not
|
|
permitted to be called until after the module init is complete,
|
|
which does not happen until after this file is processed. Once that
|
|
init is finished, Module.onRuntimeInitialized() will be
|
|
triggered. All app-level init code should go into that callback or
|
|
be triggered via it. Calling wasm-bound functions before that
|
|
callback is run will trigger an assertion in the wasm environment.
|
|
*/
|
|
window.Module.onRuntimeInitialized = function(){
|
|
'use strict';
|
|
const Module = window.Module /* wasm module as set up by emscripten */;
|
|
delete Module.onRuntimeInitialized;
|
|
|
|
/* querySelectorAll() proxy */
|
|
const EAll = function(/*[element=document,] cssSelector*/){
|
|
return (arguments.length>1 ? arguments[0] : document)
|
|
.querySelectorAll(arguments[arguments.length-1]);
|
|
};
|
|
/* querySelector() proxy */
|
|
const E = function(/*[element=document,] cssSelector*/){
|
|
return (arguments.length>1 ? arguments[0] : document)
|
|
.querySelector(arguments[arguments.length-1]);
|
|
};
|
|
|
|
// Unhide all elements which start out hidden
|
|
EAll('.initially-hidden').forEach((e)=>e.classList.remove('initially-hidden'));
|
|
|
|
const taInput = E('#input');
|
|
const btnClearIn = E('#btn-clear');
|
|
btnClearIn.addEventListener('click',function(){
|
|
taInput.value = '';
|
|
},false);
|
|
// Ctrl-enter and shift-enter both run the current SQL.
|
|
taInput.addEventListener('keydown',function(ev){
|
|
if((ev.ctrlKey || ev.shiftKey) && 13 === ev.keyCode){
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
btnRun.click();
|
|
}
|
|
}, false);
|
|
const taOutput = E('#output');
|
|
const btnClearOut = E('#btn-clear-output');
|
|
btnClearOut.addEventListener('click',function(){
|
|
taOutput.value = '';
|
|
if(Module.jqTerm) Module.jqTerm.clear();
|
|
},false);
|
|
/* Sends the given text to the shell. If it's null or empty, this
|
|
is a no-op except that the very first call will initialize the
|
|
db and output an informational header. */
|
|
const doExec = function f(sql){
|
|
if(!f._) f._ = Module.cwrap('fiddle_exec', null, ['string']);
|
|
if(Module._isDead){
|
|
Module.printErr("shell module has exit()ed. Cannot run SQL.");
|
|
return;
|
|
}
|
|
if(Module.config.autoClearOutput) taOutput.value='';
|
|
f._(sql);
|
|
};
|
|
const btnRun = E('#btn-run');
|
|
btnRun.addEventListener('click',function(){
|
|
const sql = taInput.value.trim();
|
|
if(sql){
|
|
doExec(sql);
|
|
}
|
|
},false);
|
|
|
|
const mainWrapper = E('#main-wrapper');
|
|
/* For each checkboxes with data-csstgt, set up a handler which
|
|
toggles the given CSS class on the element matching
|
|
E(data-csstgt). */
|
|
EAll('input[type=checkbox][data-csstgt]')
|
|
.forEach(function(e){
|
|
const tgt = E(e.dataset.csstgt);
|
|
const cssClass = e.dataset.cssclass || 'error';
|
|
e.checked = tgt.classList.contains(cssClass);
|
|
e.addEventListener('change', function(){
|
|
tgt.classList[
|
|
this.checked ? 'add' : 'remove'
|
|
](cssClass)
|
|
}, false);
|
|
});
|
|
/* For each checkbox with data-config=X, set up a binding to
|
|
Module.config[X]. These must be set up AFTER data-csstgt
|
|
checkboxes so that those two states can be synced properly. */
|
|
EAll('input[type=checkbox][data-config]')
|
|
.forEach(function(e){
|
|
const confVal = !!Module.config[e.dataset.config];
|
|
if(e.checked !== confVal){
|
|
/* Ensure that data-csstgt mappings (if any) get
|
|
synced properly. */
|
|
e.checked = confVal;
|
|
e.dispatchEvent(new Event('change'));
|
|
}
|
|
e.addEventListener('change', function(){
|
|
Module.config[this.dataset.config] = this.checked;
|
|
}, false);
|
|
});
|
|
/* For each button with data-cmd=X, map a click handler which
|
|
calls doExec(X). */
|
|
const cmdClick = function(){doExec(this.dataset.cmd);};
|
|
EAll('button[data-cmd]').forEach(
|
|
e => e.addEventListener('click', cmdClick, false)
|
|
);
|
|
|
|
|
|
/**
|
|
Given a DOM element, this routine measures its "effective
|
|
height", which is the bounding top/bottom range of this element
|
|
and all of its children, recursively. For some DOM structure
|
|
cases, a parent may have a reported height of 0 even though
|
|
children have non-0 sizes.
|
|
|
|
Returns 0 if !e or if the element really has no height.
|
|
*/
|
|
const effectiveHeight = function f(e){
|
|
if(!e) return 0;
|
|
if(!f.measure){
|
|
f.measure = function callee(e, depth){
|
|
if(!e) return;
|
|
const m = e.getBoundingClientRect();
|
|
if(0===depth){
|
|
callee.top = m.top;
|
|
callee.bottom = m.bottom;
|
|
}else{
|
|
callee.top = m.top ? Math.min(callee.top, m.top) : callee.top;
|
|
callee.bottom = Math.max(callee.bottom, m.bottom);
|
|
}
|
|
Array.prototype.forEach.call(e.children,(e)=>callee(e,depth+1));
|
|
if(0===depth){
|
|
//console.debug("measure() height:",e.className, callee.top, callee.bottom, (callee.bottom - callee.top));
|
|
f.extra += callee.bottom - callee.top;
|
|
}
|
|
return f.extra;
|
|
};
|
|
}
|
|
f.extra = 0;
|
|
f.measure(e,0);
|
|
return f.extra;
|
|
};
|
|
|
|
/**
|
|
Returns a function, that, as long as it continues to be invoked,
|
|
will not be triggered. The function will be called after it stops
|
|
being called for N milliseconds. If `immediate` is passed, call
|
|
the callback immediately and hinder future invocations until at
|
|
least the given time has passed.
|
|
|
|
If passed only 1 argument, or passed a falsy 2nd argument,
|
|
the default wait time set in this function's $defaultDelay
|
|
property is used.
|
|
|
|
Source: underscore.js, by way of https://davidwalsh.name/javascript-debounce-function
|
|
*/
|
|
const debounce = function f(func, wait, immediate) {
|
|
var timeout;
|
|
if(!wait) wait = f.$defaultDelay;
|
|
return function() {
|
|
const context = this, args = Array.prototype.slice.call(arguments);
|
|
const later = function() {
|
|
timeout = undefined;
|
|
if(!immediate) func.apply(context, args);
|
|
};
|
|
const callNow = immediate && !timeout;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
if(callNow) func.apply(context, args);
|
|
};
|
|
};
|
|
debounce.$defaultDelay = 500 /*arbitrary*/;
|
|
|
|
const ForceResizeKludge = (function(){
|
|
/* Workaround for Safari mayhem regarding use of vh CSS units....
|
|
We cannot use vh units to set the terminal area size because
|
|
Safari chokes on that, so we calculate that height here. Larger
|
|
than ~95% is too big for Firefox on Android, causing the input
|
|
area to move off-screen. */
|
|
const bcl = document.body.classList;
|
|
const appViews = EAll('.app-view');
|
|
const resized = function f(){
|
|
if(f.$disabled) return;
|
|
const wh = window.innerHeight;
|
|
var ht;
|
|
var extra = 0;
|
|
const elemsToCount = [
|
|
E('body > header'),
|
|
E('body > footer')
|
|
];
|
|
elemsToCount.forEach((e)=>e ? extra += effectiveHeight(e) : false);
|
|
ht = wh - extra;
|
|
appViews.forEach(function(e){
|
|
e.style.height =
|
|
e.style.maxHeight = [
|
|
"calc(", (ht>=100 ? ht : 100), "px",
|
|
" - 2em"/*fudge value*/,")"
|
|
/* ^^^^ hypothetically not needed, but both
|
|
Chrome/FF on Linux will force scrollbars on the
|
|
body if this value is too small. */
|
|
].join('');
|
|
});
|
|
};
|
|
resized.$disabled = true/*gets deleted when setup is finished*/;
|
|
window.addEventListener('resize', debounce(resized, 250), false);
|
|
return resized;
|
|
})();
|
|
|
|
/** Set up a selection list of examples */
|
|
(function(){
|
|
const xElem = E('#select-examples');
|
|
const examples = [
|
|
{name: "Timer on", sql: ".timer on"},
|
|
{name: "Setup table T", sql:`.nullvalue NULL
|
|
CREATE TABLE t(a,b);
|
|
INSERT INTO t(a,b) VALUES('abc',123),('def',456),(NULL,789),('ghi',012);
|
|
SELECT * FROM t;`},
|
|
{name: "Table list", sql: ".tables"},
|
|
{name: "Box Mode", sql: ".mode box"},
|
|
{name: "JSON Mode", sql: ".mode json"},
|
|
{name: "Mandlebrot", sql: `WITH RECURSIVE
|
|
xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2),
|
|
yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0),
|
|
m(iter, cx, cy, x, y) AS (
|
|
SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis
|
|
UNION ALL
|
|
SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m
|
|
WHERE (x*x + y*y) < 4.0 AND iter<28
|
|
),
|
|
m2(iter, cx, cy) AS (
|
|
SELECT max(iter), cx, cy FROM m GROUP BY cx, cy
|
|
),
|
|
a(t) AS (
|
|
SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '')
|
|
FROM m2 GROUP BY cy
|
|
)
|
|
SELECT group_concat(rtrim(t),x'0a') as Mandelbrot FROM a;`}
|
|
];
|
|
const newOpt = function(lbl,val){
|
|
const o = document.createElement('option');
|
|
o.value = val;
|
|
if(!val) o.setAttribute('disabled',true);
|
|
o.appendChild(document.createTextNode(lbl));
|
|
xElem.appendChild(o);
|
|
};
|
|
newOpt("Examples (replaces input!)");
|
|
examples.forEach((o)=>newOpt(o.name, o.sql));
|
|
//xElem.setAttribute('disabled',true);
|
|
xElem.selectedIndex = 0;
|
|
xElem.addEventListener('change', function(){
|
|
taInput.value = '-- ' +
|
|
this.selectedOptions[0].innerText +
|
|
'\n' + this.value;
|
|
//doExec(this.value);
|
|
});
|
|
})()/* example queries */;
|
|
|
|
Module.print(null/*clear any output generated by the init process*/);
|
|
if(window.jQuery && window.jQuery.terminal){
|
|
/* Set up the terminal-style view... */
|
|
const eTerm = window.jQuery('#view-terminal').empty();
|
|
Module.jqTerm = eTerm.terminal(doExec,{
|
|
prompt: 'sqlite> ',
|
|
greetings: false /* note that the docs incorrectly call this 'greeting' */
|
|
});
|
|
/* Set up a button to toggle the views... */
|
|
const head = E('header#titlebar');
|
|
const btnToggleView = document.createElement('button');
|
|
btnToggleView.appendChild(document.createTextNode("Toggle View"));
|
|
btnToggleView.addEventListener('click',function f(){
|
|
EAll('.app-view').forEach(e=>e.classList.toggle('hidden'));
|
|
if(document.body.classList.toggle('terminal-mode')){
|
|
ForceResizeKludge();
|
|
}
|
|
}, false);
|
|
btnToggleView.click()/*default to terminal view*/;
|
|
}
|
|
doExec(null/*init the db and output the header*/);
|
|
Module.print('\nThis experimental app is provided in the hope that it',
|
|
'may prove interesting or useful but is not an officially',
|
|
'supported deliverable of the sqlite project. It is subject to',
|
|
'any number of changes or outright removal at any time.\n');
|
|
delete ForceResizeKludge.$disabled;
|
|
ForceResizeKludge();
|
|
};
|