summaryrefslogtreecommitdiffstats
path: root/ext/wasm/fiddle/fiddle.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-05 17:28:19 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-05 17:28:19 +0000
commit18657a960e125336f704ea058e25c27bd3900dcb (patch)
tree17b438b680ed45a996d7b59951e6aa34023783f2 /ext/wasm/fiddle/fiddle.js
parentInitial commit. (diff)
downloadsqlite3-upstream.tar.xz
sqlite3-upstream.zip
Adding upstream version 3.40.1.upstream/3.40.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ext/wasm/fiddle/fiddle.js')
-rw-r--r--ext/wasm/fiddle/fiddle.js815
1 files changed, 815 insertions, 0 deletions
diff --git a/ext/wasm/fiddle/fiddle.js b/ext/wasm/fiddle/fiddle.js
new file mode 100644
index 0000000..2a3d174
--- /dev/null
+++ b/ext/wasm/fiddle/fiddle.js
@@ -0,0 +1,815 @@
+/*
+ 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, "&amp;");
+ //text = text.replace(/</g, "&lt;");
+ //text = text.replace(/>/g, "&gt;");
+ //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()*/;
+})();