eb97743cc7
FossilOrigin-Name: 7be78dd4efc410f13ff1ceda1fad82b309cc24de2d5106c8bd6b2adeaa13b106
816 lines
31 KiB
JavaScript
816 lines
31 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 main entry point for the sqlite3 fiddle app. It sets up the
|
|
various UI bits, loads a Worker for the db connection, and manages the
|
|
communication between the UI and worker.
|
|
*/
|
|
(function(){
|
|
'use strict';
|
|
/* Recall that the 'self' symbol, except where locally
|
|
overwritten, refers to the global window or worker object. */
|
|
|
|
const storage = (function(NS/*namespace object in which to store this module*/){
|
|
/* Pedantic licensing note: this code originated in the Fossil SCM
|
|
source tree, where it has a different license, but the person who
|
|
ported it into sqlite is the same one who wrote it for fossil. */
|
|
'use strict';
|
|
NS = NS||{};
|
|
|
|
/**
|
|
This module provides a basic wrapper around localStorage
|
|
or sessionStorage or a dummy proxy object if neither
|
|
of those are available.
|
|
*/
|
|
const tryStorage = function f(obj){
|
|
if(!f.key) f.key = 'storage.access.check';
|
|
try{
|
|
obj.setItem(f.key, 'f');
|
|
const x = obj.getItem(f.key);
|
|
obj.removeItem(f.key);
|
|
if(x!=='f') throw new Error(f.key+" failed")
|
|
return obj;
|
|
}catch(e){
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
/** Internal storage impl for this module. */
|
|
const $storage =
|
|
tryStorage(window.localStorage)
|
|
|| tryStorage(window.sessionStorage)
|
|
|| tryStorage({
|
|
// A basic dummy xyzStorage stand-in
|
|
$$$:{},
|
|
setItem: function(k,v){this.$$$[k]=v},
|
|
getItem: function(k){
|
|
return this.$$$.hasOwnProperty(k) ? this.$$$[k] : undefined;
|
|
},
|
|
removeItem: function(k){delete this.$$$[k]},
|
|
clear: function(){this.$$$={}}
|
|
});
|
|
|
|
/**
|
|
For the dummy storage we need to differentiate between
|
|
$storage and its real property storage for hasOwnProperty()
|
|
to work properly...
|
|
*/
|
|
const $storageHolder = $storage.hasOwnProperty('$$$') ? $storage.$$$ : $storage;
|
|
|
|
/**
|
|
A prefix which gets internally applied to all storage module
|
|
property keys so that localStorage and sessionStorage across the
|
|
same browser profile instance do not "leak" across multiple apps
|
|
being hosted by the same origin server. Such cross-polination is
|
|
still there but, with this key prefix applied, it won't be
|
|
immediately visible via the storage API.
|
|
|
|
With this in place we can justify using localStorage instead of
|
|
sessionStorage.
|
|
|
|
One implication of using localStorage and sessionStorage is that
|
|
their scope (the same "origin" and client application/profile)
|
|
allows multiple apps on the same origin to use the same
|
|
storage. Thus /appA/foo could then see changes made via
|
|
/appB/foo. The data do not cross user- or browser boundaries,
|
|
though, so it "might" arguably be called a
|
|
feature. storageKeyPrefix was added so that we can sandbox that
|
|
state for each separate app which shares an origin.
|
|
|
|
See: https://fossil-scm.org/forum/forumpost/4afc4d34de
|
|
|
|
Sidebar: it might seem odd to provide a key prefix and stick all
|
|
properties in the topmost level of the storage object. We do that
|
|
because adding a layer of object to sandbox each app would mean
|
|
(de)serializing that whole tree on every storage property change.
|
|
e.g. instead of storageObject.projectName.foo we have
|
|
storageObject[storageKeyPrefix+'foo']. That's soley for
|
|
efficiency's sake (in terms of battery life and
|
|
environment-internal storage-level effort).
|
|
*/
|
|
const storageKeyPrefix = (
|
|
$storageHolder===$storage/*localStorage or sessionStorage*/
|
|
? (
|
|
(NS.config ?
|
|
(NS.config.projectCode || NS.config.projectName
|
|
|| NS.config.shortProjectName)
|
|
: false)
|
|
|| window.location.pathname
|
|
)+'::' : (
|
|
'' /* transient storage */
|
|
)
|
|
);
|
|
|
|
/**
|
|
A proxy for localStorage or sessionStorage or a
|
|
page-instance-local proxy, if neither one is availble.
|
|
|
|
Which exact storage implementation is uses is unspecified, and
|
|
apps must not rely on it.
|
|
*/
|
|
NS.storage = {
|
|
storageKeyPrefix: storageKeyPrefix,
|
|
/** Sets the storage key k to value v, implicitly converting
|
|
it to a string. */
|
|
set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v),
|
|
/** Sets storage key k to JSON.stringify(v). */
|
|
setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
|
|
/** Returns the value for the given storage key, or
|
|
dflt if the key is not found in the storage. */
|
|
get: (k,dflt)=>$storageHolder.hasOwnProperty(
|
|
storageKeyPrefix+k
|
|
) ? $storage.getItem(storageKeyPrefix+k) : dflt,
|
|
/** Returns true if the given key has a value of "true". If the
|
|
key is not found, it returns true if the boolean value of dflt
|
|
is "true". (Remember that JS persistent storage values are all
|
|
strings.) */
|
|
getBool: function(k,dflt){
|
|
return 'true'===this.get(k,''+(!!dflt));
|
|
},
|
|
/** Returns the JSON.parse()'d value of the given
|
|
storage key's value, or dflt is the key is not
|
|
found or JSON.parse() fails. */
|
|
getJSON: function f(k,dflt){
|
|
try {
|
|
const x = this.get(k,f);
|
|
return x===f ? dflt : JSON.parse(x);
|
|
}
|
|
catch(e){return dflt}
|
|
},
|
|
/** Returns true if the storage contains the given key,
|
|
else false. */
|
|
contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k),
|
|
/** Removes the given key from the storage. Returns this. */
|
|
remove: function(k){
|
|
$storage.removeItem(storageKeyPrefix+k);
|
|
return this;
|
|
},
|
|
/** Clears ALL keys from the storage. Returns this. */
|
|
clear: function(){
|
|
this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k));
|
|
return this;
|
|
},
|
|
/** Returns an array of all keys currently in the storage. */
|
|
keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
|
|
/** Returns true if this storage is transient (only available
|
|
until the page is reloaded), indicating that fileStorage
|
|
and sessionStorage are unavailable. */
|
|
isTransient: ()=>$storageHolder!==$storage,
|
|
/** Returns a symbolic name for the current storage mechanism. */
|
|
storageImplName: function(){
|
|
if($storage===window.localStorage) return 'localStorage';
|
|
else if($storage===window.sessionStorage) return 'sessionStorage';
|
|
else return 'transient';
|
|
},
|
|
|
|
/**
|
|
Returns a brief help text string for the currently-selected
|
|
storage type.
|
|
*/
|
|
storageHelpDescription: function(){
|
|
return {
|
|
localStorage: "Browser-local persistent storage with an "+
|
|
"unspecified long-term lifetime (survives closing the browser, "+
|
|
"but maybe not a browser upgrade).",
|
|
sessionStorage: "Storage local to this browser tab, "+
|
|
"lost if this tab is closed.",
|
|
"transient": "Transient storage local to this invocation of this page."
|
|
}[this.storageImplName()];
|
|
}
|
|
};
|
|
return NS.storage;
|
|
})({})/*storage API setup*/;
|
|
|
|
|
|
/** Name of the stored copy of SqliteFiddle.config. */
|
|
const configStorageKey = 'sqlite3-fiddle-config';
|
|
|
|
/**
|
|
The SqliteFiddle object is intended to be the primary
|
|
app-level object for the main-thread side of the sqlite
|
|
fiddle application. It uses a worker thread to load the
|
|
sqlite WASM module and communicate with it.
|
|
*/
|
|
const SF/*local convenience alias*/
|
|
= window.SqliteFiddle/*canonical name*/ = {
|
|
/* Config options. */
|
|
config: {
|
|
/* If true, SqliteFiddle.echo() will auto-scroll the
|
|
output widget to the bottom when it receives output,
|
|
else it won't. */
|
|
autoScrollOutput: true,
|
|
/* If true, the output area will be cleared before each
|
|
command is run, else it will not. */
|
|
autoClearOutput: false,
|
|
/* If true, SqliteFiddle.echo() will echo its output to
|
|
the console, in addition to its normal output widget.
|
|
That slows it down but is useful for testing. */
|
|
echoToConsole: false,
|
|
/* If true, display input/output areas side-by-side. */
|
|
sideBySide: true,
|
|
/* If true, swap positions of the input/output areas. */
|
|
swapInOut: false
|
|
},
|
|
/**
|
|
Emits the given text, followed by a line break, to the
|
|
output widget. If given more than one argument, they are
|
|
join()'d together with a space between each. As a special
|
|
case, if passed a single array, that array is used in place
|
|
of the arguments array (this is to facilitate receiving
|
|
lists of arguments via worker events).
|
|
*/
|
|
echo: function f(text) {
|
|
/* Maintenance reminder: we currently require/expect a textarea
|
|
output element. It might be nice to extend this to behave
|
|
differently if the output element is a non-textarea element,
|
|
in which case it would need to append the given text as a TEXT
|
|
node and add a line break. */
|
|
if(!f._){
|
|
f._ = document.getElementById('output');
|
|
f._.value = ''; // clear browser cache
|
|
}
|
|
if(arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
|
|
else if(1===arguments.length && Array.isArray(text)) text = text.join(' ');
|
|
// These replacements are necessary if you render to raw HTML
|
|
//text = text.replace(/&/g, "&");
|
|
//text = text.replace(/</g, "<");
|
|
//text = text.replace(/>/g, ">");
|
|
//text = text.replace('\n', '<br>', 'g');
|
|
if(null===text){/*special case: clear output*/
|
|
f._.value = '';
|
|
return;
|
|
}else if(this.echo._clearPending){
|
|
delete this.echo._clearPending;
|
|
f._.value = '';
|
|
}
|
|
if(this.config.echoToConsole) console.log(text);
|
|
if(this.jqTerm) this.jqTerm.echo(text);
|
|
f._.value += text + "\n";
|
|
if(this.config.autoScrollOutput){
|
|
f._.scrollTop = f._.scrollHeight;
|
|
}
|
|
},
|
|
_msgMap: {},
|
|
/** Adds a worker message handler for messages of the given
|
|
type. */
|
|
addMsgHandler: function f(type,callback){
|
|
if(Array.isArray(type)){
|
|
type.forEach((t)=>this.addMsgHandler(t, callback));
|
|
return this;
|
|
}
|
|
(this._msgMap.hasOwnProperty(type)
|
|
? this._msgMap[type]
|
|
: (this._msgMap[type] = [])).push(callback);
|
|
return this;
|
|
},
|
|
/** Given a worker message, runs all handlers for msg.type. */
|
|
runMsgHandlers: function(msg){
|
|
const list = (this._msgMap.hasOwnProperty(msg.type)
|
|
? this._msgMap[msg.type] : false);
|
|
if(!list){
|
|
console.warn("No handlers found for message type:",msg);
|
|
return false;
|
|
}
|
|
//console.debug("runMsgHandlers",msg);
|
|
list.forEach((f)=>f(msg));
|
|
return true;
|
|
},
|
|
/** Removes all message handlers for the given message type. */
|
|
clearMsgHandlers: function(type){
|
|
delete this._msgMap[type];
|
|
return this;
|
|
},
|
|
/* Posts a message in the form {type, data} to the db worker. Returns this. */
|
|
wMsg: function(type,data,transferables){
|
|
this.worker.postMessage({type, data}, transferables || []);
|
|
return this;
|
|
},
|
|
/**
|
|
Prompts for confirmation and, if accepted, deletes
|
|
all content and tables in the (transient) database.
|
|
*/
|
|
resetDb: function(){
|
|
if(window.confirm("Really destroy all content and tables "
|
|
+"in the (transient) db?")){
|
|
this.wMsg('db-reset');
|
|
}
|
|
return this;
|
|
},
|
|
/** Stores this object's config in the browser's storage. */
|
|
storeConfig: function(){
|
|
storage.setJSON(configStorageKey,this.config);
|
|
}
|
|
};
|
|
|
|
if(1){ /* Restore SF.config */
|
|
const storedConfig = storage.getJSON(configStorageKey);
|
|
if(storedConfig){
|
|
/* Copy all properties to SF.config which are currently in
|
|
storedConfig. We don't bother copying any other
|
|
properties: those have been removed from the app in the
|
|
meantime. */
|
|
Object.keys(SF.config).forEach(function(k){
|
|
if(storedConfig.hasOwnProperty(k)){
|
|
SF.config[k] = storedConfig[k];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
SF.worker = new Worker('fiddle-worker.js'+self.location.search);
|
|
SF.worker.onmessage = (ev)=>SF.runMsgHandlers(ev.data);
|
|
SF.addMsgHandler(['stdout', 'stderr'], (ev)=>SF.echo(ev.data));
|
|
|
|
/* 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]);
|
|
};
|
|
|
|
/** Handles status updates from the Emscripten Module object. */
|
|
SF.addMsgHandler('module', function f(ev){
|
|
ev = ev.data;
|
|
if('status'!==ev.type){
|
|
console.warn("Unexpected module-type message:",ev);
|
|
return;
|
|
}
|
|
if(!f.ui){
|
|
f.ui = {
|
|
status: E('#module-status'),
|
|
progress: E('#module-progress'),
|
|
spinner: E('#module-spinner')
|
|
};
|
|
}
|
|
const msg = ev.data;
|
|
if(f.ui.progres){
|
|
progress.value = msg.step;
|
|
progress.max = msg.step + 1/*we don't know how many steps to expect*/;
|
|
}
|
|
if(1==msg.step){
|
|
f.ui.progress.classList.remove('hidden');
|
|
f.ui.spinner.classList.remove('hidden');
|
|
}
|
|
if(msg.text){
|
|
f.ui.status.classList.remove('hidden');
|
|
f.ui.status.innerText = msg.text;
|
|
}else{
|
|
if(f.ui.progress){
|
|
f.ui.progress.remove();
|
|
f.ui.spinner.remove();
|
|
delete f.ui.progress;
|
|
delete f.ui.spinner;
|
|
}
|
|
f.ui.status.classList.add('hidden');
|
|
/* The module can post messages about fatal problems,
|
|
e.g. an exit() being triggered or assertion failure,
|
|
after the last "load" message has arrived, so
|
|
leave f.ui.status and message listener intact. */
|
|
}
|
|
});
|
|
|
|
/**
|
|
The 'fiddle-ready' event is fired (with no payload) when the
|
|
wasm module has finished loading. Interestingly, that happens
|
|
_before_ the final module:status event */
|
|
SF.addMsgHandler('fiddle-ready', function(){
|
|
SF.clearMsgHandlers('fiddle-ready');
|
|
self.onSFLoaded();
|
|
});
|
|
|
|
/**
|
|
Performs all app initialization which must wait until after the
|
|
worker module is loaded. This function removes itself when it's
|
|
called.
|
|
*/
|
|
self.onSFLoaded = function(){
|
|
delete this.onSFLoaded;
|
|
// Unhide all elements which start out hidden
|
|
EAll('.initially-hidden').forEach((e)=>e.classList.remove('initially-hidden'));
|
|
E('#btn-reset').addEventListener('click',()=>SF.resetDb());
|
|
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();
|
|
btnShellExec.click();
|
|
}
|
|
}, false);
|
|
const taOutput = E('#output');
|
|
const btnClearOut = E('#btn-clear-output');
|
|
btnClearOut.addEventListener('click',function(){
|
|
taOutput.value = '';
|
|
if(SF.jqTerm) SF.jqTerm.clear();
|
|
},false);
|
|
const btnShellExec = E('#btn-shell-exec');
|
|
btnShellExec.addEventListener('click',function(ev){
|
|
let sql;
|
|
ev.preventDefault();
|
|
if(taInput.selectionStart<taInput.selectionEnd){
|
|
sql = taInput.value.substring(taInput.selectionStart,taInput.selectionEnd).trim();
|
|
}else{
|
|
sql = taInput.value.trim();
|
|
}
|
|
if(sql) SF.dbExec(sql);
|
|
},false);
|
|
|
|
const btnInterrupt = E("#btn-interrupt");
|
|
//btnInterrupt.classList.add('hidden');
|
|
/** To be called immediately before work is sent to the
|
|
worker. Updates some UI elements. The 'working'/'end'
|
|
event will apply the inverse, undoing the bits this
|
|
function does. This impl is not in the 'working'/'start'
|
|
event handler because that event is given to us
|
|
asynchronously _after_ we need to have performed this
|
|
work.
|
|
*/
|
|
const preStartWork = function f(){
|
|
if(!f._){
|
|
const title = E('title');
|
|
f._ = {
|
|
btnLabel: btnShellExec.innerText,
|
|
pageTitle: title,
|
|
pageTitleOrig: title.innerText
|
|
};
|
|
}
|
|
f._.pageTitle.innerText = "[working...] "+f._.pageTitleOrig;
|
|
btnShellExec.setAttribute('disabled','disabled');
|
|
btnInterrupt.removeAttribute('disabled','disabled');
|
|
};
|
|
|
|
/* Sends the given text to the db module to evaluate as if it
|
|
had been entered in the sqlite3 CLI shell. If it's null or
|
|
empty, this is a no-op. */
|
|
SF.dbExec = function f(sql){
|
|
if(null!==sql && this.config.autoClearOutput){
|
|
this.echo._clearPending = true;
|
|
}
|
|
preStartWork();
|
|
this.wMsg('shellExec',sql);
|
|
};
|
|
|
|
SF.addMsgHandler('working',function f(ev){
|
|
switch(ev.data){
|
|
case 'start': /* See notes in preStartWork(). */; return;
|
|
case 'end':
|
|
preStartWork._.pageTitle.innerText = preStartWork._.pageTitleOrig;
|
|
btnShellExec.innerText = preStartWork._.btnLabel;
|
|
btnShellExec.removeAttribute('disabled');
|
|
btnInterrupt.setAttribute('disabled','disabled');
|
|
return;
|
|
}
|
|
console.warn("Unhandled 'working' event:",ev.data);
|
|
});
|
|
|
|
/* For each checkbox 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
|
|
SF.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 = !!SF.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(){
|
|
SF.config[this.dataset.config] = this.checked;
|
|
SF.storeConfig();
|
|
}, false);
|
|
});
|
|
/* For each button with data-cmd=X, map a click handler which
|
|
calls SF.dbExec(X). */
|
|
const cmdClick = function(){SF.dbExec(this.dataset.cmd);};
|
|
EAll('button[data-cmd]').forEach(
|
|
e => e.addEventListener('click', cmdClick, false)
|
|
);
|
|
|
|
btnInterrupt.addEventListener('click',function(){
|
|
SF.wMsg('interrupt');
|
|
});
|
|
|
|
/** Initiate a download of the db. */
|
|
const btnExport = E('#btn-export');
|
|
const eLoadDb = E('#load-db');
|
|
const btnLoadDb = E('#btn-load-db');
|
|
btnLoadDb.addEventListener('click', ()=>eLoadDb.click());
|
|
/**
|
|
Enables (if passed true) or disables all UI elements which
|
|
"might," if timed "just right," interfere with an
|
|
in-progress db import/export/exec operation.
|
|
*/
|
|
const enableMutatingElements = function f(enable){
|
|
if(!f._elems){
|
|
f._elems = [
|
|
/* UI elements to disable while import/export are
|
|
running. Normally the export is fast enough
|
|
that this won't matter, but we really don't
|
|
want to be reading (from outside of sqlite) the
|
|
db when the user taps btnShellExec. */
|
|
btnShellExec, btnExport, eLoadDb
|
|
];
|
|
}
|
|
f._elems.forEach( enable
|
|
? (e)=>e.removeAttribute('disabled')
|
|
: (e)=>e.setAttribute('disabled','disabled') );
|
|
};
|
|
btnExport.addEventListener('click',function(){
|
|
enableMutatingElements(false);
|
|
SF.wMsg('db-export');
|
|
});
|
|
SF.addMsgHandler('db-export', function(ev){
|
|
enableMutatingElements(true);
|
|
ev = ev.data;
|
|
if(ev.error){
|
|
SF.echo("Export failed:",ev.error);
|
|
return;
|
|
}
|
|
const blob = new Blob([ev.buffer],
|
|
{type:"application/x-sqlite3"});
|
|
const a = document.createElement('a');
|
|
document.body.appendChild(a);
|
|
a.href = window.URL.createObjectURL(blob);
|
|
a.download = ev.filename;
|
|
a.addEventListener('click',function(){
|
|
setTimeout(function(){
|
|
SF.echo("Exported (possibly auto-downloaded):",ev.filename);
|
|
window.URL.revokeObjectURL(a.href);
|
|
a.remove();
|
|
},500);
|
|
});
|
|
a.click();
|
|
});
|
|
/**
|
|
Handle load/import of an external db file.
|
|
*/
|
|
eLoadDb.addEventListener('change',function(){
|
|
const f = this.files[0];
|
|
const r = new FileReader();
|
|
const status = {loaded: 0, total: 0};
|
|
enableMutatingElements(false);
|
|
r.addEventListener('loadstart', function(){
|
|
SF.echo("Loading",f.name,"...");
|
|
});
|
|
r.addEventListener('progress', function(ev){
|
|
SF.echo("Loading progress:",ev.loaded,"of",ev.total,"bytes.");
|
|
});
|
|
const that = this;
|
|
r.addEventListener('load', function(){
|
|
enableMutatingElements(true);
|
|
SF.echo("Loaded",f.name+". Opening db...");
|
|
SF.wMsg('open',{
|
|
filename: f.name,
|
|
buffer: this.result
|
|
}, [this.result]);
|
|
});
|
|
r.addEventListener('error',function(){
|
|
enableMutatingElements(true);
|
|
SF.echo("Loading",f.name,"failed for unknown reasons.");
|
|
});
|
|
r.addEventListener('abort',function(){
|
|
enableMutatingElements(true);
|
|
SF.echo("Cancelled loading of",f.name+".");
|
|
});
|
|
r.readAsArrayBuffer(f);
|
|
});
|
|
|
|
EAll('fieldset.collapsible').forEach(function(fs){
|
|
const btnToggle = E(fs,'legend > .fieldset-toggle'),
|
|
content = EAll(fs,':scope > div');
|
|
btnToggle.addEventListener('click', function(){
|
|
fs.classList.toggle('collapsed');
|
|
content.forEach((d)=>d.classList.toggle('hidden'));
|
|
}, 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 main view
|
|
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 appViews = EAll('.app-view');
|
|
const elemsToCount = [
|
|
/* Elements which we need to always count in the
|
|
visible body size. */
|
|
E('body > header'),
|
|
E('body > footer')
|
|
];
|
|
const resized = function f(){
|
|
if(f.$disabled) return;
|
|
const wh = window.innerHeight;
|
|
var ht;
|
|
var extra = 0;
|
|
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: "Help", sql: [
|
|
"-- ================================================\n",
|
|
"-- Use ctrl-enter or shift-enter to execute sqlite3\n",
|
|
"-- shell commands and SQL.\n",
|
|
"-- If a subset of the text is currently selected,\n",
|
|
"-- only that part is executed.\n",
|
|
"-- ================================================\n",
|
|
".help\n"
|
|
]},
|
|
//{name: "Timer on", sql: ".timer on"},
|
|
// ^^^ re-enable if emscripten re-enables getrusage()
|
|
{name: "Setup table T", sql:[
|
|
".nullvalue NULL\n",
|
|
"CREATE TABLE t(a,b);\n",
|
|
"INSERT INTO t(a,b) VALUES('abc',123),('def',456),(NULL,789),('ghi',012);\n",
|
|
"SELECT * FROM t;\n"
|
|
]},
|
|
{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),\n",
|
|
" yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0),\n",
|
|
" m(iter, cx, cy, x, y) AS (\n",
|
|
" SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis\n",
|
|
" UNION ALL\n",
|
|
" SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m \n",
|
|
" WHERE (x*x + y*y) < 4.0 AND iter<28\n",
|
|
" ),\n",
|
|
" m2(iter, cx, cy) AS (\n",
|
|
" SELECT max(iter), cx, cy FROM m GROUP BY cx, cy\n",
|
|
" ),\n",
|
|
" a(t) AS (\n",
|
|
" SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '') \n",
|
|
" FROM m2 GROUP BY cy\n",
|
|
" )\n",
|
|
"SELECT group_concat(rtrim(t),x'0a') as Mandelbrot FROM a;\n",
|
|
]}
|
|
];
|
|
const newOpt = function(lbl,val){
|
|
const o = document.createElement('option');
|
|
if(Array.isArray(val)) val = val.join('');
|
|
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;
|
|
SF.dbExec(this.value);
|
|
});
|
|
})()/* example queries */;
|
|
|
|
//SF.echo(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();
|
|
SF.jqTerm = eTerm.terminal(SF.dbExec.bind(SF),{
|
|
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"));
|
|
head.appendChild(btnToggleView);
|
|
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*/;
|
|
}
|
|
SF.echo('This 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');
|
|
const urlParams = new URL(self.location.href).searchParams;
|
|
SF.dbExec(urlParams.get('sql') || null);
|
|
delete ForceResizeKludge.$disabled;
|
|
ForceResizeKludge();
|
|
}/*onSFLoaded()*/;
|
|
})();
|