18c3b7501Sstephan/* 28c3b7501Sstephan 2022-05-20 38c3b7501Sstephan 48c3b7501Sstephan The author disclaims copyright to this source code. In place of a 58c3b7501Sstephan legal notice, here is a blessing: 68c3b7501Sstephan 78c3b7501Sstephan * May you do good and not evil. 88c3b7501Sstephan * May you find forgiveness for yourself and forgive others. 98c3b7501Sstephan * May you share freely, never taking more than you give. 108c3b7501Sstephan 118c3b7501Sstephan *********************************************************************** 128c3b7501Sstephan 138c3b7501Sstephan This is the main entry point for the sqlite3 fiddle app. It sets up the 148c3b7501Sstephan various UI bits, loads a Worker for the db connection, and manages the 158c3b7501Sstephan communication between the UI and worker. 168c3b7501Sstephan*/ 178c3b7501Sstephan(function(){ 188c3b7501Sstephan 'use strict'; 198c3b7501Sstephan /* Recall that the 'self' symbol, except where locally 208c3b7501Sstephan overwritten, refers to the global window or worker object. */ 218c3b7501Sstephan 228c3b7501Sstephan const storage = (function(NS/*namespace object in which to store this module*/){ 238c3b7501Sstephan /* Pedantic licensing note: this code originated in the Fossil SCM 248c3b7501Sstephan source tree, where it has a different license, but the person who 258c3b7501Sstephan ported it into sqlite is the same one who wrote it for fossil. */ 268c3b7501Sstephan 'use strict'; 278c3b7501Sstephan NS = NS||{}; 288c3b7501Sstephan 298c3b7501Sstephan /** 308c3b7501Sstephan This module provides a basic wrapper around localStorage 318c3b7501Sstephan or sessionStorage or a dummy proxy object if neither 328c3b7501Sstephan of those are available. 338c3b7501Sstephan */ 348c3b7501Sstephan const tryStorage = function f(obj){ 358c3b7501Sstephan if(!f.key) f.key = 'storage.access.check'; 368c3b7501Sstephan try{ 378c3b7501Sstephan obj.setItem(f.key, 'f'); 388c3b7501Sstephan const x = obj.getItem(f.key); 398c3b7501Sstephan obj.removeItem(f.key); 408c3b7501Sstephan if(x!=='f') throw new Error(f.key+" failed") 418c3b7501Sstephan return obj; 428c3b7501Sstephan }catch(e){ 438c3b7501Sstephan return undefined; 448c3b7501Sstephan } 458c3b7501Sstephan }; 468c3b7501Sstephan 478c3b7501Sstephan /** Internal storage impl for this module. */ 488c3b7501Sstephan const $storage = 498c3b7501Sstephan tryStorage(window.localStorage) 508c3b7501Sstephan || tryStorage(window.sessionStorage) 518c3b7501Sstephan || tryStorage({ 528c3b7501Sstephan // A basic dummy xyzStorage stand-in 538c3b7501Sstephan $$$:{}, 548c3b7501Sstephan setItem: function(k,v){this.$$$[k]=v}, 558c3b7501Sstephan getItem: function(k){ 568c3b7501Sstephan return this.$$$.hasOwnProperty(k) ? this.$$$[k] : undefined; 578c3b7501Sstephan }, 588c3b7501Sstephan removeItem: function(k){delete this.$$$[k]}, 598c3b7501Sstephan clear: function(){this.$$$={}} 608c3b7501Sstephan }); 618c3b7501Sstephan 628c3b7501Sstephan /** 638c3b7501Sstephan For the dummy storage we need to differentiate between 648c3b7501Sstephan $storage and its real property storage for hasOwnProperty() 658c3b7501Sstephan to work properly... 668c3b7501Sstephan */ 678c3b7501Sstephan const $storageHolder = $storage.hasOwnProperty('$$$') ? $storage.$$$ : $storage; 688c3b7501Sstephan 698c3b7501Sstephan /** 708c3b7501Sstephan A prefix which gets internally applied to all storage module 718c3b7501Sstephan property keys so that localStorage and sessionStorage across the 728c3b7501Sstephan same browser profile instance do not "leak" across multiple apps 738c3b7501Sstephan being hosted by the same origin server. Such cross-polination is 748c3b7501Sstephan still there but, with this key prefix applied, it won't be 758c3b7501Sstephan immediately visible via the storage API. 768c3b7501Sstephan 778c3b7501Sstephan With this in place we can justify using localStorage instead of 788c3b7501Sstephan sessionStorage. 798c3b7501Sstephan 808c3b7501Sstephan One implication of using localStorage and sessionStorage is that 818c3b7501Sstephan their scope (the same "origin" and client application/profile) 828c3b7501Sstephan allows multiple apps on the same origin to use the same 838c3b7501Sstephan storage. Thus /appA/foo could then see changes made via 848c3b7501Sstephan /appB/foo. The data do not cross user- or browser boundaries, 858c3b7501Sstephan though, so it "might" arguably be called a 868c3b7501Sstephan feature. storageKeyPrefix was added so that we can sandbox that 878c3b7501Sstephan state for each separate app which shares an origin. 888c3b7501Sstephan 898c3b7501Sstephan See: https://fossil-scm.org/forum/forumpost/4afc4d34de 908c3b7501Sstephan 918c3b7501Sstephan Sidebar: it might seem odd to provide a key prefix and stick all 928c3b7501Sstephan properties in the topmost level of the storage object. We do that 938c3b7501Sstephan because adding a layer of object to sandbox each app would mean 948c3b7501Sstephan (de)serializing that whole tree on every storage property change. 958c3b7501Sstephan e.g. instead of storageObject.projectName.foo we have 968c3b7501Sstephan storageObject[storageKeyPrefix+'foo']. That's soley for 978c3b7501Sstephan efficiency's sake (in terms of battery life and 988c3b7501Sstephan environment-internal storage-level effort). 998c3b7501Sstephan */ 1008c3b7501Sstephan const storageKeyPrefix = ( 1018c3b7501Sstephan $storageHolder===$storage/*localStorage or sessionStorage*/ 1028c3b7501Sstephan ? ( 1038c3b7501Sstephan (NS.config ? 1048c3b7501Sstephan (NS.config.projectCode || NS.config.projectName 1058c3b7501Sstephan || NS.config.shortProjectName) 1068c3b7501Sstephan : false) 1078c3b7501Sstephan || window.location.pathname 1088c3b7501Sstephan )+'::' : ( 1098c3b7501Sstephan '' /* transient storage */ 1108c3b7501Sstephan ) 1118c3b7501Sstephan ); 1128c3b7501Sstephan 1138c3b7501Sstephan /** 1148c3b7501Sstephan A proxy for localStorage or sessionStorage or a 1158c3b7501Sstephan page-instance-local proxy, if neither one is availble. 1168c3b7501Sstephan 1178c3b7501Sstephan Which exact storage implementation is uses is unspecified, and 1188c3b7501Sstephan apps must not rely on it. 1198c3b7501Sstephan */ 1208c3b7501Sstephan NS.storage = { 1218c3b7501Sstephan storageKeyPrefix: storageKeyPrefix, 1228c3b7501Sstephan /** Sets the storage key k to value v, implicitly converting 1238c3b7501Sstephan it to a string. */ 1248c3b7501Sstephan set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v), 1258c3b7501Sstephan /** Sets storage key k to JSON.stringify(v). */ 1268c3b7501Sstephan setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)), 1278c3b7501Sstephan /** Returns the value for the given storage key, or 1288c3b7501Sstephan dflt if the key is not found in the storage. */ 1298c3b7501Sstephan get: (k,dflt)=>$storageHolder.hasOwnProperty( 1308c3b7501Sstephan storageKeyPrefix+k 1318c3b7501Sstephan ) ? $storage.getItem(storageKeyPrefix+k) : dflt, 1328c3b7501Sstephan /** Returns true if the given key has a value of "true". If the 1338c3b7501Sstephan key is not found, it returns true if the boolean value of dflt 1348c3b7501Sstephan is "true". (Remember that JS persistent storage values are all 1358c3b7501Sstephan strings.) */ 1368c3b7501Sstephan getBool: function(k,dflt){ 1378c3b7501Sstephan return 'true'===this.get(k,''+(!!dflt)); 1388c3b7501Sstephan }, 1398c3b7501Sstephan /** Returns the JSON.parse()'d value of the given 1408c3b7501Sstephan storage key's value, or dflt is the key is not 1418c3b7501Sstephan found or JSON.parse() fails. */ 1428c3b7501Sstephan getJSON: function f(k,dflt){ 1438c3b7501Sstephan try { 1448c3b7501Sstephan const x = this.get(k,f); 1458c3b7501Sstephan return x===f ? dflt : JSON.parse(x); 1468c3b7501Sstephan } 1478c3b7501Sstephan catch(e){return dflt} 1488c3b7501Sstephan }, 1498c3b7501Sstephan /** Returns true if the storage contains the given key, 1508c3b7501Sstephan else false. */ 1518c3b7501Sstephan contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k), 1528c3b7501Sstephan /** Removes the given key from the storage. Returns this. */ 1538c3b7501Sstephan remove: function(k){ 1548c3b7501Sstephan $storage.removeItem(storageKeyPrefix+k); 1558c3b7501Sstephan return this; 1568c3b7501Sstephan }, 1578c3b7501Sstephan /** Clears ALL keys from the storage. Returns this. */ 1588c3b7501Sstephan clear: function(){ 1598c3b7501Sstephan this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k)); 1608c3b7501Sstephan return this; 1618c3b7501Sstephan }, 1628c3b7501Sstephan /** Returns an array of all keys currently in the storage. */ 1638c3b7501Sstephan keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)), 1648c3b7501Sstephan /** Returns true if this storage is transient (only available 1658c3b7501Sstephan until the page is reloaded), indicating that fileStorage 1668c3b7501Sstephan and sessionStorage are unavailable. */ 1678c3b7501Sstephan isTransient: ()=>$storageHolder!==$storage, 1688c3b7501Sstephan /** Returns a symbolic name for the current storage mechanism. */ 1698c3b7501Sstephan storageImplName: function(){ 1708c3b7501Sstephan if($storage===window.localStorage) return 'localStorage'; 1718c3b7501Sstephan else if($storage===window.sessionStorage) return 'sessionStorage'; 1728c3b7501Sstephan else return 'transient'; 1738c3b7501Sstephan }, 1748c3b7501Sstephan 1758c3b7501Sstephan /** 1768c3b7501Sstephan Returns a brief help text string for the currently-selected 1778c3b7501Sstephan storage type. 1788c3b7501Sstephan */ 1798c3b7501Sstephan storageHelpDescription: function(){ 1808c3b7501Sstephan return { 1818c3b7501Sstephan localStorage: "Browser-local persistent storage with an "+ 1828c3b7501Sstephan "unspecified long-term lifetime (survives closing the browser, "+ 1838c3b7501Sstephan "but maybe not a browser upgrade).", 1848c3b7501Sstephan sessionStorage: "Storage local to this browser tab, "+ 1858c3b7501Sstephan "lost if this tab is closed.", 1868c3b7501Sstephan "transient": "Transient storage local to this invocation of this page." 1878c3b7501Sstephan }[this.storageImplName()]; 1888c3b7501Sstephan } 1898c3b7501Sstephan }; 1908c3b7501Sstephan return NS.storage; 1918c3b7501Sstephan })({})/*storage API setup*/; 1928c3b7501Sstephan 1938c3b7501Sstephan 1948c3b7501Sstephan /** Name of the stored copy of SqliteFiddle.config. */ 1958c3b7501Sstephan const configStorageKey = 'sqlite3-fiddle-config'; 1968c3b7501Sstephan 1978c3b7501Sstephan /** 1988c3b7501Sstephan The SqliteFiddle object is intended to be the primary 1998c3b7501Sstephan app-level object for the main-thread side of the sqlite 2008c3b7501Sstephan fiddle application. It uses a worker thread to load the 2018c3b7501Sstephan sqlite WASM module and communicate with it. 2028c3b7501Sstephan */ 2038c3b7501Sstephan const SF/*local convenience alias*/ 2048c3b7501Sstephan = window.SqliteFiddle/*canonical name*/ = { 2058c3b7501Sstephan /* Config options. */ 2068c3b7501Sstephan config: { 2078c3b7501Sstephan /* If true, SqliteFiddle.echo() will auto-scroll the 2088c3b7501Sstephan output widget to the bottom when it receives output, 2098c3b7501Sstephan else it won't. */ 2108c3b7501Sstephan autoScrollOutput: true, 2118c3b7501Sstephan /* If true, the output area will be cleared before each 2128c3b7501Sstephan command is run, else it will not. */ 2138c3b7501Sstephan autoClearOutput: false, 2148c3b7501Sstephan /* If true, SqliteFiddle.echo() will echo its output to 2158c3b7501Sstephan the console, in addition to its normal output widget. 2168c3b7501Sstephan That slows it down but is useful for testing. */ 2178c3b7501Sstephan echoToConsole: false, 2188c3b7501Sstephan /* If true, display input/output areas side-by-side. */ 2198c3b7501Sstephan sideBySide: true, 2208c3b7501Sstephan /* If true, swap positions of the input/output areas. */ 2218c3b7501Sstephan swapInOut: false 2228c3b7501Sstephan }, 2238c3b7501Sstephan /** 2248c3b7501Sstephan Emits the given text, followed by a line break, to the 2258c3b7501Sstephan output widget. If given more than one argument, they are 2268c3b7501Sstephan join()'d together with a space between each. As a special 2278c3b7501Sstephan case, if passed a single array, that array is used in place 2288c3b7501Sstephan of the arguments array (this is to facilitate receiving 2298c3b7501Sstephan lists of arguments via worker events). 2308c3b7501Sstephan */ 2318c3b7501Sstephan echo: function f(text) { 2328c3b7501Sstephan /* Maintenance reminder: we currently require/expect a textarea 2338c3b7501Sstephan output element. It might be nice to extend this to behave 2348c3b7501Sstephan differently if the output element is a non-textarea element, 2358c3b7501Sstephan in which case it would need to append the given text as a TEXT 2368c3b7501Sstephan node and add a line break. */ 2378c3b7501Sstephan if(!f._){ 2388c3b7501Sstephan f._ = document.getElementById('output'); 2398c3b7501Sstephan f._.value = ''; // clear browser cache 2408c3b7501Sstephan } 2418c3b7501Sstephan if(arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); 2428c3b7501Sstephan else if(1===arguments.length && Array.isArray(text)) text = text.join(' '); 2438c3b7501Sstephan // These replacements are necessary if you render to raw HTML 2448c3b7501Sstephan //text = text.replace(/&/g, "&"); 2458c3b7501Sstephan //text = text.replace(/</g, "<"); 2468c3b7501Sstephan //text = text.replace(/>/g, ">"); 2478c3b7501Sstephan //text = text.replace('\n', '<br>', 'g'); 2488c3b7501Sstephan if(null===text){/*special case: clear output*/ 2498c3b7501Sstephan f._.value = ''; 2508c3b7501Sstephan return; 2518c3b7501Sstephan }else if(this.echo._clearPending){ 2528c3b7501Sstephan delete this.echo._clearPending; 2538c3b7501Sstephan f._.value = ''; 2548c3b7501Sstephan } 2558c3b7501Sstephan if(this.config.echoToConsole) console.log(text); 2568c3b7501Sstephan if(this.jqTerm) this.jqTerm.echo(text); 2578c3b7501Sstephan f._.value += text + "\n"; 2588c3b7501Sstephan if(this.config.autoScrollOutput){ 2598c3b7501Sstephan f._.scrollTop = f._.scrollHeight; 2608c3b7501Sstephan } 2618c3b7501Sstephan }, 2628c3b7501Sstephan _msgMap: {}, 2638c3b7501Sstephan /** Adds a worker message handler for messages of the given 2648c3b7501Sstephan type. */ 2658c3b7501Sstephan addMsgHandler: function f(type,callback){ 2668c3b7501Sstephan if(Array.isArray(type)){ 2678c3b7501Sstephan type.forEach((t)=>this.addMsgHandler(t, callback)); 2688c3b7501Sstephan return this; 2698c3b7501Sstephan } 2708c3b7501Sstephan (this._msgMap.hasOwnProperty(type) 2718c3b7501Sstephan ? this._msgMap[type] 2728c3b7501Sstephan : (this._msgMap[type] = [])).push(callback); 2738c3b7501Sstephan return this; 2748c3b7501Sstephan }, 2758c3b7501Sstephan /** Given a worker message, runs all handlers for msg.type. */ 2768c3b7501Sstephan runMsgHandlers: function(msg){ 2778c3b7501Sstephan const list = (this._msgMap.hasOwnProperty(msg.type) 2788c3b7501Sstephan ? this._msgMap[msg.type] : false); 2798c3b7501Sstephan if(!list){ 2808c3b7501Sstephan console.warn("No handlers found for message type:",msg); 2818c3b7501Sstephan return false; 2828c3b7501Sstephan } 2838c3b7501Sstephan //console.debug("runMsgHandlers",msg); 2848c3b7501Sstephan list.forEach((f)=>f(msg)); 2858c3b7501Sstephan return true; 2868c3b7501Sstephan }, 2878c3b7501Sstephan /** Removes all message handlers for the given message type. */ 2888c3b7501Sstephan clearMsgHandlers: function(type){ 2898c3b7501Sstephan delete this._msgMap[type]; 2908c3b7501Sstephan return this; 2918c3b7501Sstephan }, 2928c3b7501Sstephan /* Posts a message in the form {type, data} to the db worker. Returns this. */ 293395012e5Sstephan wMsg: function(type,data,transferables){ 294395012e5Sstephan this.worker.postMessage({type, data}, transferables || []); 2958c3b7501Sstephan return this; 2968c3b7501Sstephan }, 2978c3b7501Sstephan /** 2988c3b7501Sstephan Prompts for confirmation and, if accepted, deletes 2998c3b7501Sstephan all content and tables in the (transient) database. 3008c3b7501Sstephan */ 3018c3b7501Sstephan resetDb: function(){ 3028c3b7501Sstephan if(window.confirm("Really destroy all content and tables " 3038c3b7501Sstephan +"in the (transient) db?")){ 3048c3b7501Sstephan this.wMsg('db-reset'); 3058c3b7501Sstephan } 3068c3b7501Sstephan return this; 3078c3b7501Sstephan }, 3088c3b7501Sstephan /** Stores this object's config in the browser's storage. */ 3098c3b7501Sstephan storeConfig: function(){ 3108c3b7501Sstephan storage.setJSON(configStorageKey,this.config); 3118c3b7501Sstephan } 3128c3b7501Sstephan }; 3138c3b7501Sstephan 3148c3b7501Sstephan if(1){ /* Restore SF.config */ 3158c3b7501Sstephan const storedConfig = storage.getJSON(configStorageKey); 3168c3b7501Sstephan if(storedConfig){ 3178c3b7501Sstephan /* Copy all properties to SF.config which are currently in 3188c3b7501Sstephan storedConfig. We don't bother copying any other 3198c3b7501Sstephan properties: those have been removed from the app in the 3208c3b7501Sstephan meantime. */ 3218c3b7501Sstephan Object.keys(SF.config).forEach(function(k){ 3228c3b7501Sstephan if(storedConfig.hasOwnProperty(k)){ 3238c3b7501Sstephan SF.config[k] = storedConfig[k]; 3248c3b7501Sstephan } 3258c3b7501Sstephan }); 3268c3b7501Sstephan } 3278c3b7501Sstephan } 3288c3b7501Sstephan 3296110a5d0Sstephan SF.worker = new Worker('fiddle-worker.js'+self.location.search); 3308c3b7501Sstephan SF.worker.onmessage = (ev)=>SF.runMsgHandlers(ev.data); 3318c3b7501Sstephan SF.addMsgHandler(['stdout', 'stderr'], (ev)=>SF.echo(ev.data)); 3328c3b7501Sstephan 3338c3b7501Sstephan /* querySelectorAll() proxy */ 3348c3b7501Sstephan const EAll = function(/*[element=document,] cssSelector*/){ 3358c3b7501Sstephan return (arguments.length>1 ? arguments[0] : document) 3368c3b7501Sstephan .querySelectorAll(arguments[arguments.length-1]); 3378c3b7501Sstephan }; 3388c3b7501Sstephan /* querySelector() proxy */ 3398c3b7501Sstephan const E = function(/*[element=document,] cssSelector*/){ 3408c3b7501Sstephan return (arguments.length>1 ? arguments[0] : document) 3418c3b7501Sstephan .querySelector(arguments[arguments.length-1]); 3428c3b7501Sstephan }; 3438c3b7501Sstephan 344*eb97743cSstephan /** Handles status updates from the Emscripten Module object. */ 3458c3b7501Sstephan SF.addMsgHandler('module', function f(ev){ 3468c3b7501Sstephan ev = ev.data; 3478c3b7501Sstephan if('status'!==ev.type){ 3488c3b7501Sstephan console.warn("Unexpected module-type message:",ev); 3498c3b7501Sstephan return; 3508c3b7501Sstephan } 3518c3b7501Sstephan if(!f.ui){ 3528c3b7501Sstephan f.ui = { 3538c3b7501Sstephan status: E('#module-status'), 3548c3b7501Sstephan progress: E('#module-progress'), 3558c3b7501Sstephan spinner: E('#module-spinner') 3568c3b7501Sstephan }; 3578c3b7501Sstephan } 3588c3b7501Sstephan const msg = ev.data; 3598c3b7501Sstephan if(f.ui.progres){ 3608c3b7501Sstephan progress.value = msg.step; 3618c3b7501Sstephan progress.max = msg.step + 1/*we don't know how many steps to expect*/; 3628c3b7501Sstephan } 3638c3b7501Sstephan if(1==msg.step){ 3648c3b7501Sstephan f.ui.progress.classList.remove('hidden'); 3658c3b7501Sstephan f.ui.spinner.classList.remove('hidden'); 3668c3b7501Sstephan } 3678c3b7501Sstephan if(msg.text){ 3688c3b7501Sstephan f.ui.status.classList.remove('hidden'); 3698c3b7501Sstephan f.ui.status.innerText = msg.text; 3708c3b7501Sstephan }else{ 3718c3b7501Sstephan if(f.ui.progress){ 3728c3b7501Sstephan f.ui.progress.remove(); 3738c3b7501Sstephan f.ui.spinner.remove(); 3748c3b7501Sstephan delete f.ui.progress; 3758c3b7501Sstephan delete f.ui.spinner; 3768c3b7501Sstephan } 3778c3b7501Sstephan f.ui.status.classList.add('hidden'); 3788c3b7501Sstephan /* The module can post messages about fatal problems, 3798c3b7501Sstephan e.g. an exit() being triggered or assertion failure, 3808c3b7501Sstephan after the last "load" message has arrived, so 3818c3b7501Sstephan leave f.ui.status and message listener intact. */ 3828c3b7501Sstephan } 3838c3b7501Sstephan }); 3848c3b7501Sstephan 3858c3b7501Sstephan /** 3868c3b7501Sstephan The 'fiddle-ready' event is fired (with no payload) when the 3878c3b7501Sstephan wasm module has finished loading. Interestingly, that happens 3888c3b7501Sstephan _before_ the final module:status event */ 3898c3b7501Sstephan SF.addMsgHandler('fiddle-ready', function(){ 3908c3b7501Sstephan SF.clearMsgHandlers('fiddle-ready'); 3918c3b7501Sstephan self.onSFLoaded(); 3928c3b7501Sstephan }); 3938c3b7501Sstephan 3948c3b7501Sstephan /** 3958c3b7501Sstephan Performs all app initialization which must wait until after the 3968c3b7501Sstephan worker module is loaded. This function removes itself when it's 3978c3b7501Sstephan called. 3988c3b7501Sstephan */ 3998c3b7501Sstephan self.onSFLoaded = function(){ 4008c3b7501Sstephan delete this.onSFLoaded; 4018c3b7501Sstephan // Unhide all elements which start out hidden 4028c3b7501Sstephan EAll('.initially-hidden').forEach((e)=>e.classList.remove('initially-hidden')); 4038c3b7501Sstephan E('#btn-reset').addEventListener('click',()=>SF.resetDb()); 4048c3b7501Sstephan const taInput = E('#input'); 4058c3b7501Sstephan const btnClearIn = E('#btn-clear'); 4068c3b7501Sstephan btnClearIn.addEventListener('click',function(){ 4078c3b7501Sstephan taInput.value = ''; 4088c3b7501Sstephan },false); 4098c3b7501Sstephan // Ctrl-enter and shift-enter both run the current SQL. 4108c3b7501Sstephan taInput.addEventListener('keydown',function(ev){ 4118c3b7501Sstephan if((ev.ctrlKey || ev.shiftKey) && 13 === ev.keyCode){ 4128c3b7501Sstephan ev.preventDefault(); 4138c3b7501Sstephan ev.stopPropagation(); 4148c3b7501Sstephan btnShellExec.click(); 4158c3b7501Sstephan } 4168c3b7501Sstephan }, false); 4178c3b7501Sstephan const taOutput = E('#output'); 4188c3b7501Sstephan const btnClearOut = E('#btn-clear-output'); 4198c3b7501Sstephan btnClearOut.addEventListener('click',function(){ 4208c3b7501Sstephan taOutput.value = ''; 4218c3b7501Sstephan if(SF.jqTerm) SF.jqTerm.clear(); 4228c3b7501Sstephan },false); 4238c3b7501Sstephan const btnShellExec = E('#btn-shell-exec'); 4248c3b7501Sstephan btnShellExec.addEventListener('click',function(ev){ 4258c3b7501Sstephan let sql; 4268c3b7501Sstephan ev.preventDefault(); 4278c3b7501Sstephan if(taInput.selectionStart<taInput.selectionEnd){ 4288c3b7501Sstephan sql = taInput.value.substring(taInput.selectionStart,taInput.selectionEnd).trim(); 4298c3b7501Sstephan }else{ 4308c3b7501Sstephan sql = taInput.value.trim(); 4318c3b7501Sstephan } 4328c3b7501Sstephan if(sql) SF.dbExec(sql); 4338c3b7501Sstephan },false); 4348c3b7501Sstephan 4358c3b7501Sstephan const btnInterrupt = E("#btn-interrupt"); 4368c3b7501Sstephan //btnInterrupt.classList.add('hidden'); 4378c3b7501Sstephan /** To be called immediately before work is sent to the 4388c3b7501Sstephan worker. Updates some UI elements. The 'working'/'end' 4398c3b7501Sstephan event will apply the inverse, undoing the bits this 4408c3b7501Sstephan function does. This impl is not in the 'working'/'start' 4418c3b7501Sstephan event handler because that event is given to us 4428c3b7501Sstephan asynchronously _after_ we need to have performed this 4438c3b7501Sstephan work. 4448c3b7501Sstephan */ 4458c3b7501Sstephan const preStartWork = function f(){ 4468c3b7501Sstephan if(!f._){ 4478c3b7501Sstephan const title = E('title'); 4488c3b7501Sstephan f._ = { 4498c3b7501Sstephan btnLabel: btnShellExec.innerText, 4508c3b7501Sstephan pageTitle: title, 4518c3b7501Sstephan pageTitleOrig: title.innerText 4528c3b7501Sstephan }; 4538c3b7501Sstephan } 4548c3b7501Sstephan f._.pageTitle.innerText = "[working...] "+f._.pageTitleOrig; 4558c3b7501Sstephan btnShellExec.setAttribute('disabled','disabled'); 4568c3b7501Sstephan btnInterrupt.removeAttribute('disabled','disabled'); 4578c3b7501Sstephan }; 4588c3b7501Sstephan 4598c3b7501Sstephan /* Sends the given text to the db module to evaluate as if it 4608c3b7501Sstephan had been entered in the sqlite3 CLI shell. If it's null or 46160d9aa7cSstephan empty, this is a no-op. */ 4628c3b7501Sstephan SF.dbExec = function f(sql){ 46360d9aa7cSstephan if(null!==sql && this.config.autoClearOutput){ 4648c3b7501Sstephan this.echo._clearPending = true; 4658c3b7501Sstephan } 4668c3b7501Sstephan preStartWork(); 4678c3b7501Sstephan this.wMsg('shellExec',sql); 4688c3b7501Sstephan }; 4698c3b7501Sstephan 4708c3b7501Sstephan SF.addMsgHandler('working',function f(ev){ 4718c3b7501Sstephan switch(ev.data){ 4728c3b7501Sstephan case 'start': /* See notes in preStartWork(). */; return; 4738c3b7501Sstephan case 'end': 4748c3b7501Sstephan preStartWork._.pageTitle.innerText = preStartWork._.pageTitleOrig; 4758c3b7501Sstephan btnShellExec.innerText = preStartWork._.btnLabel; 4768c3b7501Sstephan btnShellExec.removeAttribute('disabled'); 4778c3b7501Sstephan btnInterrupt.setAttribute('disabled','disabled'); 4788c3b7501Sstephan return; 4798c3b7501Sstephan } 4808c3b7501Sstephan console.warn("Unhandled 'working' event:",ev.data); 4818c3b7501Sstephan }); 4828c3b7501Sstephan 4838c3b7501Sstephan /* For each checkbox with data-csstgt, set up a handler which 4848c3b7501Sstephan toggles the given CSS class on the element matching 4858c3b7501Sstephan E(data-csstgt). */ 4868c3b7501Sstephan EAll('input[type=checkbox][data-csstgt]') 4878c3b7501Sstephan .forEach(function(e){ 4888c3b7501Sstephan const tgt = E(e.dataset.csstgt); 4898c3b7501Sstephan const cssClass = e.dataset.cssclass || 'error'; 4908c3b7501Sstephan e.checked = tgt.classList.contains(cssClass); 4918c3b7501Sstephan e.addEventListener('change', function(){ 4928c3b7501Sstephan tgt.classList[ 4938c3b7501Sstephan this.checked ? 'add' : 'remove' 4948c3b7501Sstephan ](cssClass) 4958c3b7501Sstephan }, false); 4968c3b7501Sstephan }); 4978c3b7501Sstephan /* For each checkbox with data-config=X, set up a binding to 4988c3b7501Sstephan SF.config[X]. These must be set up AFTER data-csstgt 4998c3b7501Sstephan checkboxes so that those two states can be synced properly. */ 5008c3b7501Sstephan EAll('input[type=checkbox][data-config]') 5018c3b7501Sstephan .forEach(function(e){ 5028c3b7501Sstephan const confVal = !!SF.config[e.dataset.config]; 5038c3b7501Sstephan if(e.checked !== confVal){ 5048c3b7501Sstephan /* Ensure that data-csstgt mappings (if any) get 5058c3b7501Sstephan synced properly. */ 5068c3b7501Sstephan e.checked = confVal; 5078c3b7501Sstephan e.dispatchEvent(new Event('change')); 5088c3b7501Sstephan } 5098c3b7501Sstephan e.addEventListener('change', function(){ 5108c3b7501Sstephan SF.config[this.dataset.config] = this.checked; 5118c3b7501Sstephan SF.storeConfig(); 5128c3b7501Sstephan }, false); 5138c3b7501Sstephan }); 5148c3b7501Sstephan /* For each button with data-cmd=X, map a click handler which 5158c3b7501Sstephan calls SF.dbExec(X). */ 5168c3b7501Sstephan const cmdClick = function(){SF.dbExec(this.dataset.cmd);}; 5178c3b7501Sstephan EAll('button[data-cmd]').forEach( 5188c3b7501Sstephan e => e.addEventListener('click', cmdClick, false) 5198c3b7501Sstephan ); 5208c3b7501Sstephan 5218c3b7501Sstephan btnInterrupt.addEventListener('click',function(){ 5228c3b7501Sstephan SF.wMsg('interrupt'); 5238c3b7501Sstephan }); 5248c3b7501Sstephan 5258c3b7501Sstephan /** Initiate a download of the db. */ 5268c3b7501Sstephan const btnExport = E('#btn-export'); 5278c3b7501Sstephan const eLoadDb = E('#load-db'); 5288c3b7501Sstephan const btnLoadDb = E('#btn-load-db'); 5298c3b7501Sstephan btnLoadDb.addEventListener('click', ()=>eLoadDb.click()); 5308c3b7501Sstephan /** 5318c3b7501Sstephan Enables (if passed true) or disables all UI elements which 5328c3b7501Sstephan "might," if timed "just right," interfere with an 5338c3b7501Sstephan in-progress db import/export/exec operation. 5348c3b7501Sstephan */ 5358c3b7501Sstephan const enableMutatingElements = function f(enable){ 5368c3b7501Sstephan if(!f._elems){ 5378c3b7501Sstephan f._elems = [ 5388c3b7501Sstephan /* UI elements to disable while import/export are 5398c3b7501Sstephan running. Normally the export is fast enough 5408c3b7501Sstephan that this won't matter, but we really don't 5418c3b7501Sstephan want to be reading (from outside of sqlite) the 5428c3b7501Sstephan db when the user taps btnShellExec. */ 5438c3b7501Sstephan btnShellExec, btnExport, eLoadDb 5448c3b7501Sstephan ]; 5458c3b7501Sstephan } 5468c3b7501Sstephan f._elems.forEach( enable 5478c3b7501Sstephan ? (e)=>e.removeAttribute('disabled') 5488c3b7501Sstephan : (e)=>e.setAttribute('disabled','disabled') ); 5498c3b7501Sstephan }; 5508c3b7501Sstephan btnExport.addEventListener('click',function(){ 5518c3b7501Sstephan enableMutatingElements(false); 5528c3b7501Sstephan SF.wMsg('db-export'); 5538c3b7501Sstephan }); 5548c3b7501Sstephan SF.addMsgHandler('db-export', function(ev){ 5558c3b7501Sstephan enableMutatingElements(true); 5568c3b7501Sstephan ev = ev.data; 5578c3b7501Sstephan if(ev.error){ 5588c3b7501Sstephan SF.echo("Export failed:",ev.error); 5598c3b7501Sstephan return; 5608c3b7501Sstephan } 561395012e5Sstephan const blob = new Blob([ev.buffer], 562395012e5Sstephan {type:"application/x-sqlite3"}); 5638c3b7501Sstephan const a = document.createElement('a'); 5648c3b7501Sstephan document.body.appendChild(a); 5658c3b7501Sstephan a.href = window.URL.createObjectURL(blob); 5668c3b7501Sstephan a.download = ev.filename; 5678c3b7501Sstephan a.addEventListener('click',function(){ 5688c3b7501Sstephan setTimeout(function(){ 5698c3b7501Sstephan SF.echo("Exported (possibly auto-downloaded):",ev.filename); 5708c3b7501Sstephan window.URL.revokeObjectURL(a.href); 5718c3b7501Sstephan a.remove(); 5728c3b7501Sstephan },500); 5738c3b7501Sstephan }); 5748c3b7501Sstephan a.click(); 5758c3b7501Sstephan }); 5768c3b7501Sstephan /** 5778c3b7501Sstephan Handle load/import of an external db file. 5788c3b7501Sstephan */ 5798c3b7501Sstephan eLoadDb.addEventListener('change',function(){ 5808c3b7501Sstephan const f = this.files[0]; 5818c3b7501Sstephan const r = new FileReader(); 5828c3b7501Sstephan const status = {loaded: 0, total: 0}; 5838c3b7501Sstephan enableMutatingElements(false); 5848c3b7501Sstephan r.addEventListener('loadstart', function(){ 5858c3b7501Sstephan SF.echo("Loading",f.name,"..."); 5868c3b7501Sstephan }); 5878c3b7501Sstephan r.addEventListener('progress', function(ev){ 5888c3b7501Sstephan SF.echo("Loading progress:",ev.loaded,"of",ev.total,"bytes."); 5898c3b7501Sstephan }); 5908c3b7501Sstephan const that = this; 5918c3b7501Sstephan r.addEventListener('load', function(){ 5928c3b7501Sstephan enableMutatingElements(true); 5938c3b7501Sstephan SF.echo("Loaded",f.name+". Opening db..."); 5948c3b7501Sstephan SF.wMsg('open',{ 5958c3b7501Sstephan filename: f.name, 5968c3b7501Sstephan buffer: this.result 597395012e5Sstephan }, [this.result]); 5988c3b7501Sstephan }); 5998c3b7501Sstephan r.addEventListener('error',function(){ 6008c3b7501Sstephan enableMutatingElements(true); 6018c3b7501Sstephan SF.echo("Loading",f.name,"failed for unknown reasons."); 6028c3b7501Sstephan }); 6038c3b7501Sstephan r.addEventListener('abort',function(){ 6048c3b7501Sstephan enableMutatingElements(true); 6058c3b7501Sstephan SF.echo("Cancelled loading of",f.name+"."); 6068c3b7501Sstephan }); 6078c3b7501Sstephan r.readAsArrayBuffer(f); 6088c3b7501Sstephan }); 6098c3b7501Sstephan 6108c3b7501Sstephan EAll('fieldset.collapsible').forEach(function(fs){ 6118c3b7501Sstephan const btnToggle = E(fs,'legend > .fieldset-toggle'), 6128c3b7501Sstephan content = EAll(fs,':scope > div'); 6138c3b7501Sstephan btnToggle.addEventListener('click', function(){ 6148c3b7501Sstephan fs.classList.toggle('collapsed'); 6158c3b7501Sstephan content.forEach((d)=>d.classList.toggle('hidden')); 6168c3b7501Sstephan }, false); 6178c3b7501Sstephan }); 6188c3b7501Sstephan 6198c3b7501Sstephan /** 6208c3b7501Sstephan Given a DOM element, this routine measures its "effective 6218c3b7501Sstephan height", which is the bounding top/bottom range of this element 6228c3b7501Sstephan and all of its children, recursively. For some DOM structure 6238c3b7501Sstephan cases, a parent may have a reported height of 0 even though 6248c3b7501Sstephan children have non-0 sizes. 6258c3b7501Sstephan 6268c3b7501Sstephan Returns 0 if !e or if the element really has no height. 6278c3b7501Sstephan */ 6288c3b7501Sstephan const effectiveHeight = function f(e){ 6298c3b7501Sstephan if(!e) return 0; 6308c3b7501Sstephan if(!f.measure){ 6318c3b7501Sstephan f.measure = function callee(e, depth){ 6328c3b7501Sstephan if(!e) return; 6338c3b7501Sstephan const m = e.getBoundingClientRect(); 6348c3b7501Sstephan if(0===depth){ 6358c3b7501Sstephan callee.top = m.top; 6368c3b7501Sstephan callee.bottom = m.bottom; 6378c3b7501Sstephan }else{ 6388c3b7501Sstephan callee.top = m.top ? Math.min(callee.top, m.top) : callee.top; 6398c3b7501Sstephan callee.bottom = Math.max(callee.bottom, m.bottom); 6408c3b7501Sstephan } 6418c3b7501Sstephan Array.prototype.forEach.call(e.children,(e)=>callee(e,depth+1)); 6428c3b7501Sstephan if(0===depth){ 6438c3b7501Sstephan //console.debug("measure() height:",e.className, callee.top, callee.bottom, (callee.bottom - callee.top)); 6448c3b7501Sstephan f.extra += callee.bottom - callee.top; 6458c3b7501Sstephan } 6468c3b7501Sstephan return f.extra; 6478c3b7501Sstephan }; 6488c3b7501Sstephan } 6498c3b7501Sstephan f.extra = 0; 6508c3b7501Sstephan f.measure(e,0); 6518c3b7501Sstephan return f.extra; 6528c3b7501Sstephan }; 6538c3b7501Sstephan 6548c3b7501Sstephan /** 6558c3b7501Sstephan Returns a function, that, as long as it continues to be invoked, 6568c3b7501Sstephan will not be triggered. The function will be called after it stops 6578c3b7501Sstephan being called for N milliseconds. If `immediate` is passed, call 6588c3b7501Sstephan the callback immediately and hinder future invocations until at 6598c3b7501Sstephan least the given time has passed. 6608c3b7501Sstephan 6618c3b7501Sstephan If passed only 1 argument, or passed a falsy 2nd argument, 6628c3b7501Sstephan the default wait time set in this function's $defaultDelay 6638c3b7501Sstephan property is used. 6648c3b7501Sstephan 6658c3b7501Sstephan Source: underscore.js, by way of https://davidwalsh.name/javascript-debounce-function 6668c3b7501Sstephan */ 6678c3b7501Sstephan const debounce = function f(func, wait, immediate) { 6688c3b7501Sstephan var timeout; 6698c3b7501Sstephan if(!wait) wait = f.$defaultDelay; 6708c3b7501Sstephan return function() { 6718c3b7501Sstephan const context = this, args = Array.prototype.slice.call(arguments); 6728c3b7501Sstephan const later = function() { 6738c3b7501Sstephan timeout = undefined; 6748c3b7501Sstephan if(!immediate) func.apply(context, args); 6758c3b7501Sstephan }; 6768c3b7501Sstephan const callNow = immediate && !timeout; 6778c3b7501Sstephan clearTimeout(timeout); 6788c3b7501Sstephan timeout = setTimeout(later, wait); 6798c3b7501Sstephan if(callNow) func.apply(context, args); 6808c3b7501Sstephan }; 6818c3b7501Sstephan }; 6828c3b7501Sstephan debounce.$defaultDelay = 500 /*arbitrary*/; 6838c3b7501Sstephan 6848c3b7501Sstephan const ForceResizeKludge = (function(){ 6858c3b7501Sstephan /* Workaround for Safari mayhem regarding use of vh CSS 6868c3b7501Sstephan units.... We cannot use vh units to set the main view 6878c3b7501Sstephan size because Safari chokes on that, so we calculate 6888c3b7501Sstephan that height here. Larger than ~95% is too big for 6898c3b7501Sstephan Firefox on Android, causing the input area to move 6908c3b7501Sstephan off-screen. */ 6918c3b7501Sstephan const appViews = EAll('.app-view'); 6928c3b7501Sstephan const elemsToCount = [ 6938c3b7501Sstephan /* Elements which we need to always count in the 6948c3b7501Sstephan visible body size. */ 6958c3b7501Sstephan E('body > header'), 6968c3b7501Sstephan E('body > footer') 6978c3b7501Sstephan ]; 6988c3b7501Sstephan const resized = function f(){ 6998c3b7501Sstephan if(f.$disabled) return; 7008c3b7501Sstephan const wh = window.innerHeight; 7018c3b7501Sstephan var ht; 7028c3b7501Sstephan var extra = 0; 7038c3b7501Sstephan elemsToCount.forEach((e)=>e ? extra += effectiveHeight(e) : false); 7048c3b7501Sstephan ht = wh - extra; 7058c3b7501Sstephan appViews.forEach(function(e){ 7068c3b7501Sstephan e.style.height = 7078c3b7501Sstephan e.style.maxHeight = [ 7088c3b7501Sstephan "calc(", (ht>=100 ? ht : 100), "px", 7098c3b7501Sstephan " - 2em"/*fudge value*/,")" 7108c3b7501Sstephan /* ^^^^ hypothetically not needed, but both 7118c3b7501Sstephan Chrome/FF on Linux will force scrollbars on the 7128c3b7501Sstephan body if this value is too small. */ 7138c3b7501Sstephan ].join(''); 7148c3b7501Sstephan }); 7158c3b7501Sstephan }; 7168c3b7501Sstephan resized.$disabled = true/*gets deleted when setup is finished*/; 7178c3b7501Sstephan window.addEventListener('resize', debounce(resized, 250), false); 7188c3b7501Sstephan return resized; 7198c3b7501Sstephan })(); 7208c3b7501Sstephan 7218c3b7501Sstephan /** Set up a selection list of examples */ 7228c3b7501Sstephan (function(){ 7238c3b7501Sstephan const xElem = E('#select-examples'); 7248c3b7501Sstephan const examples = [ 725*eb97743cSstephan {name: "Help", sql: [ 726*eb97743cSstephan "-- ================================================\n", 727*eb97743cSstephan "-- Use ctrl-enter or shift-enter to execute sqlite3\n", 728*eb97743cSstephan "-- shell commands and SQL.\n", 729*eb97743cSstephan "-- If a subset of the text is currently selected,\n", 730*eb97743cSstephan "-- only that part is executed.\n", 731*eb97743cSstephan "-- ================================================\n", 732*eb97743cSstephan ".help\n" 733*eb97743cSstephan ]}, 734429899ddSstephan //{name: "Timer on", sql: ".timer on"}, 735429899ddSstephan // ^^^ re-enable if emscripten re-enables getrusage() 736*eb97743cSstephan {name: "Setup table T", sql:[ 737*eb97743cSstephan ".nullvalue NULL\n", 738*eb97743cSstephan "CREATE TABLE t(a,b);\n", 739*eb97743cSstephan "INSERT INTO t(a,b) VALUES('abc',123),('def',456),(NULL,789),('ghi',012);\n", 740*eb97743cSstephan "SELECT * FROM t;\n" 741*eb97743cSstephan ]}, 7428c3b7501Sstephan {name: "Table list", sql: ".tables"}, 7438c3b7501Sstephan {name: "Box Mode", sql: ".mode box"}, 7448c3b7501Sstephan {name: "JSON Mode", sql: ".mode json"}, 745*eb97743cSstephan {name: "Mandlebrot", sql:[ 746*eb97743cSstephan "WITH RECURSIVE", 747*eb97743cSstephan " xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2),\n", 748*eb97743cSstephan " yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0),\n", 749*eb97743cSstephan " m(iter, cx, cy, x, y) AS (\n", 750*eb97743cSstephan " SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis\n", 751*eb97743cSstephan " UNION ALL\n", 752*eb97743cSstephan " SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m \n", 753*eb97743cSstephan " WHERE (x*x + y*y) < 4.0 AND iter<28\n", 754*eb97743cSstephan " ),\n", 755*eb97743cSstephan " m2(iter, cx, cy) AS (\n", 756*eb97743cSstephan " SELECT max(iter), cx, cy FROM m GROUP BY cx, cy\n", 757*eb97743cSstephan " ),\n", 758*eb97743cSstephan " a(t) AS (\n", 759*eb97743cSstephan " SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '') \n", 760*eb97743cSstephan " FROM m2 GROUP BY cy\n", 761*eb97743cSstephan " )\n", 762*eb97743cSstephan "SELECT group_concat(rtrim(t),x'0a') as Mandelbrot FROM a;\n", 763*eb97743cSstephan ]} 7648c3b7501Sstephan ]; 7658c3b7501Sstephan const newOpt = function(lbl,val){ 7668c3b7501Sstephan const o = document.createElement('option'); 767*eb97743cSstephan if(Array.isArray(val)) val = val.join(''); 7688c3b7501Sstephan o.value = val; 7698c3b7501Sstephan if(!val) o.setAttribute('disabled',true); 7708c3b7501Sstephan o.appendChild(document.createTextNode(lbl)); 7718c3b7501Sstephan xElem.appendChild(o); 7728c3b7501Sstephan }; 7738c3b7501Sstephan newOpt("Examples (replaces input!)"); 7748c3b7501Sstephan examples.forEach((o)=>newOpt(o.name, o.sql)); 7758c3b7501Sstephan //xElem.setAttribute('disabled',true); 7768c3b7501Sstephan xElem.selectedIndex = 0; 7778c3b7501Sstephan xElem.addEventListener('change', function(){ 7788c3b7501Sstephan taInput.value = '-- ' + 7798c3b7501Sstephan this.selectedOptions[0].innerText + 7808c3b7501Sstephan '\n' + this.value; 7818c3b7501Sstephan SF.dbExec(this.value); 7828c3b7501Sstephan }); 7838c3b7501Sstephan })()/* example queries */; 7848c3b7501Sstephan 785a579f440Sstephan //SF.echo(null/*clear any output generated by the init process*/); 7868c3b7501Sstephan if(window.jQuery && window.jQuery.terminal){ 7878c3b7501Sstephan /* Set up the terminal-style view... */ 7888c3b7501Sstephan const eTerm = window.jQuery('#view-terminal').empty(); 7898c3b7501Sstephan SF.jqTerm = eTerm.terminal(SF.dbExec.bind(SF),{ 7908c3b7501Sstephan prompt: 'sqlite> ', 7918c3b7501Sstephan greetings: false /* note that the docs incorrectly call this 'greeting' */ 7928c3b7501Sstephan }); 7938c3b7501Sstephan /* Set up a button to toggle the views... */ 7948c3b7501Sstephan const head = E('header#titlebar'); 7958c3b7501Sstephan const btnToggleView = document.createElement('button'); 7968c3b7501Sstephan btnToggleView.appendChild(document.createTextNode("Toggle View")); 7978c3b7501Sstephan head.appendChild(btnToggleView); 7988c3b7501Sstephan btnToggleView.addEventListener('click',function f(){ 7998c3b7501Sstephan EAll('.app-view').forEach(e=>e.classList.toggle('hidden')); 8008c3b7501Sstephan if(document.body.classList.toggle('terminal-mode')){ 8018c3b7501Sstephan ForceResizeKludge(); 8028c3b7501Sstephan } 8038c3b7501Sstephan }, false); 8048c3b7501Sstephan btnToggleView.click()/*default to terminal view*/; 8058c3b7501Sstephan } 8068c3b7501Sstephan SF.echo('This experimental app is provided in the hope that it', 8078c3b7501Sstephan 'may prove interesting or useful but is not an officially', 8088c3b7501Sstephan 'supported deliverable of the sqlite project. It is subject to', 8098c3b7501Sstephan 'any number of changes or outright removal at any time.\n'); 810395012e5Sstephan const urlParams = new URL(self.location.href).searchParams; 811395012e5Sstephan SF.dbExec(urlParams.get('sql') || null); 8128c3b7501Sstephan delete ForceResizeKludge.$disabled; 8138c3b7501Sstephan ForceResizeKludge(); 8148c3b7501Sstephan }/*onSFLoaded()*/; 8158c3b7501Sstephan})(); 816