Snapshot of upstream SQLite 3.46.1
[sqlcipher.git] / ext / wasm / fiddle / fiddle.js
blobd28589835c47add7d2263bebaee0200b7657c1ab
1 /*
2 2022-05-20
4 The author disclaims copyright to this source code. In place of a
5 legal notice, here is a blessing:
7 * May you do good and not evil.
8 * May you find forgiveness for yourself and forgive others.
9 * May you share freely, never taking more than you give.
11 ***********************************************************************
13 This is the main entry point for the sqlite3 fiddle app. It sets up the
14 various UI bits, loads a Worker for the db connection, and manages the
15 communication between the UI and worker.
17 (function(){
18 'use strict';
19 /* Recall that the 'self' symbol, except where locally
20 overwritten, refers to the global window or worker object. */
22 const storage = (function(NS/*namespace object in which to store this module*/){
23 /* Pedantic licensing note: this code originated in the Fossil SCM
24 source tree, where it has a different license, but the person who
25 ported it into sqlite is the same one who wrote it for fossil. */
26 'use strict';
27 NS = NS||{};
29 /**
30 This module provides a basic wrapper around localStorage
31 or sessionStorage or a dummy proxy object if neither
32 of those are available.
34 const tryStorage = function f(obj){
35 if(!f.key) f.key = 'storage.access.check';
36 try{
37 obj.setItem(f.key, 'f');
38 const x = obj.getItem(f.key);
39 obj.removeItem(f.key);
40 if(x!=='f') throw new Error(f.key+" failed")
41 return obj;
42 }catch(e){
43 return undefined;
47 /** Internal storage impl for this module. */
48 const $storage =
49 tryStorage(window.localStorage)
50 || tryStorage(window.sessionStorage)
51 || tryStorage({
52 // A basic dummy xyzStorage stand-in
53 $$$:{},
54 setItem: function(k,v){this.$$$[k]=v},
55 getItem: function(k){
56 return this.$$$.hasOwnProperty(k) ? this.$$$[k] : undefined;
58 removeItem: function(k){delete this.$$$[k]},
59 clear: function(){this.$$$={}}
60 });
62 /**
63 For the dummy storage we need to differentiate between
64 $storage and its real property storage for hasOwnProperty()
65 to work properly...
67 const $storageHolder = $storage.hasOwnProperty('$$$') ? $storage.$$$ : $storage;
69 /**
70 A prefix which gets internally applied to all storage module
71 property keys so that localStorage and sessionStorage across the
72 same browser profile instance do not "leak" across multiple apps
73 being hosted by the same origin server. Such cross-polination is
74 still there but, with this key prefix applied, it won't be
75 immediately visible via the storage API.
77 With this in place we can justify using localStorage instead of
78 sessionStorage.
80 One implication of using localStorage and sessionStorage is that
81 their scope (the same "origin" and client application/profile)
82 allows multiple apps on the same origin to use the same
83 storage. Thus /appA/foo could then see changes made via
84 /appB/foo. The data do not cross user- or browser boundaries,
85 though, so it "might" arguably be called a
86 feature. storageKeyPrefix was added so that we can sandbox that
87 state for each separate app which shares an origin.
89 See: https://fossil-scm.org/forum/forumpost/4afc4d34de
91 Sidebar: it might seem odd to provide a key prefix and stick all
92 properties in the topmost level of the storage object. We do that
93 because adding a layer of object to sandbox each app would mean
94 (de)serializing that whole tree on every storage property change.
95 e.g. instead of storageObject.projectName.foo we have
96 storageObject[storageKeyPrefix+'foo']. That's soley for
97 efficiency's sake (in terms of battery life and
98 environment-internal storage-level effort).
100 const storageKeyPrefix = (
101 $storageHolder===$storage/*localStorage or sessionStorage*/
103 (NS.config ?
104 (NS.config.projectCode || NS.config.projectName
105 || NS.config.shortProjectName)
106 : false)
107 || window.location.pathname
108 )+'::' : (
109 '' /* transient storage */
114 A proxy for localStorage or sessionStorage or a
115 page-instance-local proxy, if neither one is availble.
117 Which exact storage implementation is uses is unspecified, and
118 apps must not rely on it.
120 NS.storage = {
121 storageKeyPrefix: storageKeyPrefix,
122 /** Sets the storage key k to value v, implicitly converting
123 it to a string. */
124 set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v),
125 /** Sets storage key k to JSON.stringify(v). */
126 setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
127 /** Returns the value for the given storage key, or
128 dflt if the key is not found in the storage. */
129 get: (k,dflt)=>$storageHolder.hasOwnProperty(
130 storageKeyPrefix+k
131 ) ? $storage.getItem(storageKeyPrefix+k) : dflt,
132 /** Returns true if the given key has a value of "true". If the
133 key is not found, it returns true if the boolean value of dflt
134 is "true". (Remember that JS persistent storage values are all
135 strings.) */
136 getBool: function(k,dflt){
137 return 'true'===this.get(k,''+(!!dflt));
139 /** Returns the JSON.parse()'d value of the given
140 storage key's value, or dflt is the key is not
141 found or JSON.parse() fails. */
142 getJSON: function f(k,dflt){
143 try {
144 const x = this.get(k,f);
145 return x===f ? dflt : JSON.parse(x);
147 catch(e){return dflt}
149 /** Returns true if the storage contains the given key,
150 else false. */
151 contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k),
152 /** Removes the given key from the storage. Returns this. */
153 remove: function(k){
154 $storage.removeItem(storageKeyPrefix+k);
155 return this;
157 /** Clears ALL keys from the storage. Returns this. */
158 clear: function(){
159 this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k));
160 return this;
162 /** Returns an array of all keys currently in the storage. */
163 keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
164 /** Returns true if this storage is transient (only available
165 until the page is reloaded), indicating that fileStorage
166 and sessionStorage are unavailable. */
167 isTransient: ()=>$storageHolder!==$storage,
168 /** Returns a symbolic name for the current storage mechanism. */
169 storageImplName: function(){
170 if($storage===window.localStorage) return 'localStorage';
171 else if($storage===window.sessionStorage) return 'sessionStorage';
172 else return 'transient';
176 Returns a brief help text string for the currently-selected
177 storage type.
179 storageHelpDescription: function(){
180 return {
181 localStorage: "Browser-local persistent storage with an "+
182 "unspecified long-term lifetime (survives closing the browser, "+
183 "but maybe not a browser upgrade).",
184 sessionStorage: "Storage local to this browser tab, "+
185 "lost if this tab is closed.",
186 "transient": "Transient storage local to this invocation of this page."
187 }[this.storageImplName()];
190 return NS.storage;
191 })({})/*storage API setup*/;
194 /** Name of the stored copy of SqliteFiddle.config. */
195 const configStorageKey = 'sqlite3-fiddle-config';
198 The SqliteFiddle object is intended to be the primary
199 app-level object for the main-thread side of the sqlite
200 fiddle application. It uses a worker thread to load the
201 sqlite WASM module and communicate with it.
203 const SF/*local convenience alias*/
204 = window.SqliteFiddle/*canonical name*/ = {
205 /* Config options. */
206 config: {
207 /* If true, SqliteFiddle.echo() will auto-scroll the
208 output widget to the bottom when it receives output,
209 else it won't. */
210 autoScrollOutput: true,
211 /* If true, the output area will be cleared before each
212 command is run, else it will not. */
213 autoClearOutput: false,
214 /* If true, SqliteFiddle.echo() will echo its output to
215 the console, in addition to its normal output widget.
216 That slows it down but is useful for testing. */
217 echoToConsole: false,
218 /* If true, display input/output areas side-by-side. */
219 sideBySide: true,
220 /* If true, swap positions of the input/output areas. */
221 swapInOut: false
224 Emits the given text, followed by a line break, to the
225 output widget. If given more than one argument, they are
226 join()'d together with a space between each. As a special
227 case, if passed a single array, that array is used in place
228 of the arguments array (this is to facilitate receiving
229 lists of arguments via worker events).
231 echo: function f(text) {
232 /* Maintenance reminder: we currently require/expect a textarea
233 output element. It might be nice to extend this to behave
234 differently if the output element is a non-textarea element,
235 in which case it would need to append the given text as a TEXT
236 node and add a line break. */
237 if(!f._){
238 f._ = document.getElementById('output');
239 f._.value = ''; // clear browser cache
241 if(arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
242 else if(1===arguments.length && Array.isArray(text)) text = text.join(' ');
243 // These replacements are necessary if you render to raw HTML
244 //text = text.replace(/&/g, "&");
245 //text = text.replace(/</g, "&lt;");
246 //text = text.replace(/>/g, "&gt;");
247 //text = text.replace('\n', '<br>', 'g');
248 if(null===text){/*special case: clear output*/
249 f._.value = '';
250 return;
251 }else if(this.echo._clearPending){
252 delete this.echo._clearPending;
253 f._.value = '';
255 if(this.config.echoToConsole) console.log(text);
256 if(this.jqTerm) this.jqTerm.echo(text);
257 f._.value += text + "\n";
258 if(this.config.autoScrollOutput){
259 f._.scrollTop = f._.scrollHeight;
262 _msgMap: {},
263 /** Adds a worker message handler for messages of the given
264 type. */
265 addMsgHandler: function f(type,callback){
266 if(Array.isArray(type)){
267 type.forEach((t)=>this.addMsgHandler(t, callback));
268 return this;
270 (this._msgMap.hasOwnProperty(type)
271 ? this._msgMap[type]
272 : (this._msgMap[type] = [])).push(callback);
273 return this;
275 /** Given a worker message, runs all handlers for msg.type. */
276 runMsgHandlers: function(msg){
277 const list = (this._msgMap.hasOwnProperty(msg.type)
278 ? this._msgMap[msg.type] : false);
279 if(!list){
280 console.warn("No handlers found for message type:",msg);
281 return false;
283 //console.debug("runMsgHandlers",msg);
284 list.forEach((f)=>f(msg));
285 return true;
287 /** Removes all message handlers for the given message type. */
288 clearMsgHandlers: function(type){
289 delete this._msgMap[type];
290 return this;
292 /* Posts a message in the form {type, data} to the db worker. Returns this. */
293 wMsg: function(type,data,transferables){
294 this.worker.postMessage({type, data}, transferables || []);
295 return this;
298 Prompts for confirmation and, if accepted, deletes
299 all content and tables in the (transient) database.
301 resetDb: function(){
302 if(window.confirm("Really destroy all content and tables "
303 +"in the (transient) db?")){
304 this.wMsg('db-reset');
306 return this;
308 /** Stores this object's config in the browser's storage. */
309 storeConfig: function(){
310 storage.setJSON(configStorageKey,this.config);
314 if(1){ /* Restore SF.config */
315 const storedConfig = storage.getJSON(configStorageKey);
316 if(storedConfig){
317 /* Copy all properties to SF.config which are currently in
318 storedConfig. We don't bother copying any other
319 properties: those have been removed from the app in the
320 meantime. */
321 Object.keys(SF.config).forEach(function(k){
322 if(storedConfig.hasOwnProperty(k)){
323 SF.config[k] = storedConfig[k];
329 SF.worker = new Worker('fiddle-worker.js'+self.location.search);
330 SF.worker.onmessage = (ev)=>SF.runMsgHandlers(ev.data);
331 SF.addMsgHandler(['stdout', 'stderr'], (ev)=>SF.echo(ev.data));
333 /* querySelectorAll() proxy */
334 const EAll = function(/*[element=document,] cssSelector*/){
335 return (arguments.length>1 ? arguments[0] : document)
336 .querySelectorAll(arguments[arguments.length-1]);
338 /* querySelector() proxy */
339 const E = function(/*[element=document,] cssSelector*/){
340 return (arguments.length>1 ? arguments[0] : document)
341 .querySelector(arguments[arguments.length-1]);
344 /** Handles status updates from the Emscripten Module object. */
345 SF.addMsgHandler('module', function f(ev){
346 ev = ev.data;
347 if('status'!==ev.type){
348 console.warn("Unexpected module-type message:",ev);
349 return;
351 if(!f.ui){
352 f.ui = {
353 status: E('#module-status'),
354 progress: E('#module-progress'),
355 spinner: E('#module-spinner')
358 const msg = ev.data;
359 if(f.ui.progres){
360 progress.value = msg.step;
361 progress.max = msg.step + 1/*we don't know how many steps to expect*/;
363 if(1==msg.step){
364 f.ui.progress.classList.remove('hidden');
365 f.ui.spinner.classList.remove('hidden');
367 if(msg.text){
368 f.ui.status.classList.remove('hidden');
369 f.ui.status.innerText = msg.text;
370 }else{
371 if(f.ui.progress){
372 f.ui.progress.remove();
373 f.ui.spinner.remove();
374 delete f.ui.progress;
375 delete f.ui.spinner;
377 f.ui.status.classList.add('hidden');
378 /* The module can post messages about fatal problems,
379 e.g. an exit() being triggered or assertion failure,
380 after the last "load" message has arrived, so
381 leave f.ui.status and message listener intact. */
386 The 'fiddle-ready' event is fired (with no payload) when the
387 wasm module has finished loading. Interestingly, that happens
388 _before_ the final module:status event */
389 SF.addMsgHandler('fiddle-ready', function(){
390 SF.clearMsgHandlers('fiddle-ready');
391 self.onSFLoaded();
395 Performs all app initialization which must wait until after the
396 worker module is loaded. This function removes itself when it's
397 called.
399 self.onSFLoaded = function(){
400 delete this.onSFLoaded;
401 // Unhide all elements which start out hidden
402 EAll('.initially-hidden').forEach((e)=>e.classList.remove('initially-hidden'));
403 E('#btn-reset').addEventListener('click',()=>SF.resetDb());
404 const taInput = E('#input');
405 const btnClearIn = E('#btn-clear');
406 const selectExamples = E('#select-examples');
407 btnClearIn.addEventListener('click',function(){
408 taInput.value = '';
409 selectExamples.selectedIndex = 0;
410 },false);
411 // Ctrl-enter and shift-enter both run the current SQL.
412 taInput.addEventListener('keydown',function(ev){
413 if((ev.ctrlKey || ev.shiftKey) && 13 === ev.keyCode){
414 ev.preventDefault();
415 ev.stopPropagation();
416 btnShellExec.click();
418 }, false);
419 const taOutput = E('#output');
420 const btnClearOut = E('#btn-clear-output');
421 btnClearOut.addEventListener('click',function(){
422 taOutput.value = '';
423 if(SF.jqTerm) SF.jqTerm.clear();
424 },false);
425 const btnShellExec = E('#btn-shell-exec');
426 btnShellExec.addEventListener('click',function(ev){
427 let sql;
428 ev.preventDefault();
429 if(taInput.selectionStart<taInput.selectionEnd){
430 sql = taInput.value.substring(taInput.selectionStart,taInput.selectionEnd).trim();
431 }else{
432 sql = taInput.value.trim();
434 if(sql) SF.dbExec(sql);
435 },false);
437 const btnInterrupt = E("#btn-interrupt");
438 //btnInterrupt.classList.add('hidden');
439 /** To be called immediately before work is sent to the
440 worker. Updates some UI elements. The 'working'/'end'
441 event will apply the inverse, undoing the bits this
442 function does. This impl is not in the 'working'/'start'
443 event handler because that event is given to us
444 asynchronously _after_ we need to have performed this
445 work.
447 const preStartWork = function f(){
448 if(!f._){
449 const title = E('title');
450 f._ = {
451 btnLabel: btnShellExec.innerText,
452 pageTitle: title,
453 pageTitleOrig: title.innerText
456 f._.pageTitle.innerText = "[working...] "+f._.pageTitleOrig;
457 btnShellExec.setAttribute('disabled','disabled');
458 btnInterrupt.removeAttribute('disabled','disabled');
461 /* Sends the given text to the db module to evaluate as if it
462 had been entered in the sqlite3 CLI shell. If it's null or
463 empty, this is a no-op. */
464 SF.dbExec = function f(sql){
465 if(null!==sql && this.config.autoClearOutput){
466 this.echo._clearPending = true;
468 preStartWork();
469 this.wMsg('shellExec',sql);
472 SF.addMsgHandler('working',function f(ev){
473 switch(ev.data){
474 case 'start': /* See notes in preStartWork(). */; return;
475 case 'end':
476 preStartWork._.pageTitle.innerText = preStartWork._.pageTitleOrig;
477 btnShellExec.innerText = preStartWork._.btnLabel;
478 btnShellExec.removeAttribute('disabled');
479 btnInterrupt.setAttribute('disabled','disabled');
480 return;
482 console.warn("Unhandled 'working' event:",ev.data);
485 /* For each checkbox with data-csstgt, set up a handler which
486 toggles the given CSS class on the element matching
487 E(data-csstgt). */
488 EAll('input[type=checkbox][data-csstgt]')
489 .forEach(function(e){
490 const tgt = E(e.dataset.csstgt);
491 const cssClass = e.dataset.cssclass || 'error';
492 e.checked = tgt.classList.contains(cssClass);
493 e.addEventListener('change', function(){
494 tgt.classList[
495 this.checked ? 'add' : 'remove'
496 ](cssClass)
497 }, false);
499 /* For each checkbox with data-config=X, set up a binding to
500 SF.config[X]. These must be set up AFTER data-csstgt
501 checkboxes so that those two states can be synced properly. */
502 EAll('input[type=checkbox][data-config]')
503 .forEach(function(e){
504 const confVal = !!SF.config[e.dataset.config];
505 if(e.checked !== confVal){
506 /* Ensure that data-csstgt mappings (if any) get
507 synced properly. */
508 e.checked = confVal;
509 e.dispatchEvent(new Event('change'));
511 e.addEventListener('change', function(){
512 SF.config[this.dataset.config] = this.checked;
513 SF.storeConfig();
514 }, false);
516 /* For each button with data-cmd=X, map a click handler which
517 calls SF.dbExec(X). */
518 const cmdClick = function(){SF.dbExec(this.dataset.cmd);};
519 EAll('button[data-cmd]').forEach(
520 e => e.addEventListener('click', cmdClick, false)
523 btnInterrupt.addEventListener('click',function(){
524 SF.wMsg('interrupt');
527 /** Initiate a download of the db. */
528 const btnExport = E('#btn-export');
529 const eLoadDb = E('#load-db');
530 const btnLoadDb = E('#btn-load-db');
531 btnLoadDb.addEventListener('click', ()=>eLoadDb.click());
533 Enables (if passed true) or disables all UI elements which
534 "might," if timed "just right," interfere with an
535 in-progress db import/export/exec operation.
537 const enableMutatingElements = function f(enable){
538 if(!f._elems){
539 f._elems = [
540 /* UI elements to disable while import/export are
541 running. Normally the export is fast enough
542 that this won't matter, but we really don't
543 want to be reading (from outside of sqlite) the
544 db when the user taps btnShellExec. */
545 btnShellExec, btnExport, eLoadDb
548 f._elems.forEach( enable
549 ? (e)=>e.removeAttribute('disabled')
550 : (e)=>e.setAttribute('disabled','disabled') );
552 btnExport.addEventListener('click',function(){
553 enableMutatingElements(false);
554 SF.wMsg('db-export');
556 SF.addMsgHandler('db-export', function(ev){
557 enableMutatingElements(true);
558 ev = ev.data;
559 if(ev.error){
560 SF.echo("Export failed:",ev.error);
561 return;
563 const blob = new Blob([ev.buffer],
564 {type:"application/x-sqlite3"});
565 const a = document.createElement('a');
566 document.body.appendChild(a);
567 a.href = window.URL.createObjectURL(blob);
568 a.download = ev.filename;
569 a.addEventListener('click',function(){
570 setTimeout(function(){
571 SF.echo("Exported (possibly auto-downloaded):",ev.filename);
572 window.URL.revokeObjectURL(a.href);
573 a.remove();
574 },500);
576 a.click();
579 Handle load/import of an external db file.
581 eLoadDb.addEventListener('change',function(){
582 const f = this.files[0];
583 const r = new FileReader();
584 const status = {loaded: 0, total: 0};
585 enableMutatingElements(false);
586 r.addEventListener('loadstart', function(){
587 SF.echo("Loading",f.name,"...");
589 r.addEventListener('progress', function(ev){
590 SF.echo("Loading progress:",ev.loaded,"of",ev.total,"bytes.");
592 const that = this;
593 r.addEventListener('load', function(){
594 enableMutatingElements(true);
595 SF.echo("Loaded",f.name+". Opening db...");
596 SF.wMsg('open',{
597 filename: f.name,
598 buffer: this.result
599 }, [this.result]);
601 r.addEventListener('error',function(){
602 enableMutatingElements(true);
603 SF.echo("Loading",f.name,"failed for unknown reasons.");
605 r.addEventListener('abort',function(){
606 enableMutatingElements(true);
607 SF.echo("Cancelled loading of",f.name+".");
609 r.readAsArrayBuffer(f);
612 EAll('fieldset.collapsible').forEach(function(fs){
613 const btnToggle = E(fs,'legend > .fieldset-toggle'),
614 content = EAll(fs,':scope > div');
615 btnToggle.addEventListener('click', function(){
616 fs.classList.toggle('collapsed');
617 content.forEach((d)=>d.classList.toggle('hidden'));
618 }, false);
622 Given a DOM element, this routine measures its "effective
623 height", which is the bounding top/bottom range of this element
624 and all of its children, recursively. For some DOM structure
625 cases, a parent may have a reported height of 0 even though
626 children have non-0 sizes.
628 Returns 0 if !e or if the element really has no height.
630 const effectiveHeight = function f(e){
631 if(!e) return 0;
632 if(!f.measure){
633 f.measure = function callee(e, depth){
634 if(!e) return;
635 const m = e.getBoundingClientRect();
636 if(0===depth){
637 callee.top = m.top;
638 callee.bottom = m.bottom;
639 }else{
640 callee.top = m.top ? Math.min(callee.top, m.top) : callee.top;
641 callee.bottom = Math.max(callee.bottom, m.bottom);
643 Array.prototype.forEach.call(e.children,(e)=>callee(e,depth+1));
644 if(0===depth){
645 //console.debug("measure() height:",e.className, callee.top, callee.bottom, (callee.bottom - callee.top));
646 f.extra += callee.bottom - callee.top;
648 return f.extra;
651 f.extra = 0;
652 f.measure(e,0);
653 return f.extra;
657 Returns a function, that, as long as it continues to be invoked,
658 will not be triggered. The function will be called after it stops
659 being called for N milliseconds. If `immediate` is passed, call
660 the callback immediately and hinder future invocations until at
661 least the given time has passed.
663 If passed only 1 argument, or passed a falsy 2nd argument,
664 the default wait time set in this function's $defaultDelay
665 property is used.
667 Source: underscore.js, by way of https://davidwalsh.name/javascript-debounce-function
669 const debounce = function f(func, wait, immediate) {
670 var timeout;
671 if(!wait) wait = f.$defaultDelay;
672 return function() {
673 const context = this, args = Array.prototype.slice.call(arguments);
674 const later = function() {
675 timeout = undefined;
676 if(!immediate) func.apply(context, args);
678 const callNow = immediate && !timeout;
679 clearTimeout(timeout);
680 timeout = setTimeout(later, wait);
681 if(callNow) func.apply(context, args);
684 debounce.$defaultDelay = 500 /*arbitrary*/;
686 const ForceResizeKludge = (function(){
687 /* Workaround for Safari mayhem regarding use of vh CSS
688 units.... We cannot use vh units to set the main view
689 size because Safari chokes on that, so we calculate
690 that height here. Larger than ~95% is too big for
691 Firefox on Android, causing the input area to move
692 off-screen. */
693 const appViews = EAll('.app-view');
694 const elemsToCount = [
695 /* Elements which we need to always count in the
696 visible body size. */
697 E('body > header'),
698 E('body > footer')
700 const resized = function f(){
701 if(f.$disabled) return;
702 const wh = window.innerHeight;
703 var ht;
704 var extra = 0;
705 elemsToCount.forEach((e)=>e ? extra += effectiveHeight(e) : false);
706 ht = wh - extra;
707 appViews.forEach(function(e){
708 e.style.height =
709 e.style.maxHeight = [
710 "calc(", (ht>=100 ? ht : 100), "px",
711 " - 2em"/*fudge value*/,")"
712 /* ^^^^ hypothetically not needed, but both
713 Chrome/FF on Linux will force scrollbars on the
714 body if this value is too small. */
715 ].join('');
718 resized.$disabled = true/*gets deleted when setup is finished*/;
719 window.addEventListener('resize', debounce(resized, 250), false);
720 return resized;
721 })();
723 /** Set up a selection list of examples */
724 (function(){
725 const xElem = E('#select-examples');
726 const examples = [
727 {name: "Help", sql: [
728 "-- ================================================\n",
729 "-- Use ctrl-enter or shift-enter to execute sqlite3\n",
730 "-- shell commands and SQL.\n",
731 "-- If a subset of the text is currently selected,\n",
732 "-- only that part is executed.\n",
733 "-- ================================================\n",
734 ".help\n"
736 //{name: "Timer on", sql: ".timer on"},
737 // ^^^ re-enable if emscripten re-enables getrusage()
738 {name: "Box Mode", sql: ".mode box"},
739 {name: "Setup table T", sql:[
740 ".nullvalue NULL\n",
741 "CREATE TABLE t(a,b);\n",
742 "INSERT INTO t(a,b) VALUES('abc',123),('def',456),(NULL,789),('ghi',012);\n",
743 "SELECT * FROM t;\n"
745 {name: "sqlite_schema", sql: "select * from sqlite_schema"},
746 {name: "Mandelbrot", sql:[
747 "WITH RECURSIVE",
748 " xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2),\n",
749 " yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0),\n",
750 " m(iter, cx, cy, x, y) AS (\n",
751 " SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis\n",
752 " UNION ALL\n",
753 " SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m \n",
754 " WHERE (x*x + y*y) < 4.0 AND iter<28\n",
755 " ),\n",
756 " m2(iter, cx, cy) AS (\n",
757 " SELECT max(iter), cx, cy FROM m GROUP BY cx, cy\n",
758 " ),\n",
759 " a(t) AS (\n",
760 " SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '') \n",
761 " FROM m2 GROUP BY cy\n",
762 " )\n",
763 "SELECT group_concat(rtrim(t),x'0a') as Mandelbrot FROM a;\n",
765 {name: "JSON pretty-print",
766 sql: "select json_pretty(json_object('ex',json('[52,3.14159]')))"
768 {name: "JSON pretty-print (with tabs)",
769 sql: "select json_pretty(json_object('ex',json('[52,3.14159]')),char(0x09))"
772 const newOpt = function(lbl,val){
773 const o = document.createElement('option');
774 if(Array.isArray(val)) val = val.join('');
775 o.value = val;
776 if(!val) o.setAttribute('disabled',true);
777 o.appendChild(document.createTextNode(lbl));
778 xElem.appendChild(o);
780 newOpt("Examples (replaces input!)");
781 examples.forEach((o)=>newOpt(o.name, o.sql));
782 //xElem.setAttribute('disabled',true);
783 xElem.selectedIndex = 0;
784 xElem.addEventListener('change', function(){
785 taInput.value = '-- ' +
786 this.selectedOptions[0].innerText +
787 '\n' + this.value;
788 SF.dbExec(this.value);
790 })()/* example queries */;
792 //SF.echo(null/*clear any output generated by the init process*/);
793 if(window.jQuery && window.jQuery.terminal){
794 /* Set up the terminal-style view... */
795 const eTerm = window.jQuery('#view-terminal').empty();
796 SF.jqTerm = eTerm.terminal(SF.dbExec.bind(SF),{
797 prompt: 'sqlite> ',
798 greetings: false /* note that the docs incorrectly call this 'greeting' */
800 /* Set up a button to toggle the views... */
801 const head = E('header#titlebar');
802 const btnToggleView = document.createElement('button');
803 btnToggleView.appendChild(document.createTextNode("Toggle View"));
804 head.appendChild(btnToggleView);
805 btnToggleView.addEventListener('click',function f(){
806 EAll('.app-view').forEach(e=>e.classList.toggle('hidden'));
807 if(document.body.classList.toggle('terminal-mode')){
808 ForceResizeKludge();
810 }, false);
811 btnToggleView.click()/*default to terminal view*/;
813 SF.echo('This experimental app is provided in the hope that it',
814 'may prove interesting or useful but is not an officially',
815 'supported deliverable of the sqlite project. It is subject to',
816 'any number of changes or outright removal at any time.\n');
817 const urlParams = new URL(self.location.href).searchParams;
818 SF.dbExec(urlParams.get('sql') || null);
819 delete ForceResizeKludge.$disabled;
820 ForceResizeKludge();
821 }/*onSFLoaded()*/;
822 })();