xref: /sqlite-3.40.0/ext/wasm/fiddle/fiddle.js (revision 2cf599cf)
1/*
2  2022-05-20
3
4  The author disclaims copyright to this source code.  In place of a
5  legal notice, here is a blessing:
6
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.
10
11  ***********************************************************************
12
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.
16*/
17(function(){
18    'use strict';
19    /* Recall that the 'self' symbol, except where locally
20       overwritten, refers to the global window or worker object. */
21
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||{};
28
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.
33        */
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;
44            }
45        };
46
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;
57                  },
58                  removeItem: function(k){delete this.$$$[k]},
59                  clear: function(){this.$$$={}}
60              });
61
62        /**
63           For the dummy storage we need to differentiate between
64           $storage and its real property storage for hasOwnProperty()
65           to work properly...
66        */
67        const $storageHolder = $storage.hasOwnProperty('$$$') ? $storage.$$$ : $storage;
68
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.
76
77           With this in place we can justify using localStorage instead of
78           sessionStorage.
79
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.
88
89           See: https://fossil-scm.org/forum/forumpost/4afc4d34de
90
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).
99        */
100        const storageKeyPrefix = (
101            $storageHolder===$storage/*localStorage or sessionStorage*/
102                ? (
103                    (NS.config ?
104                     (NS.config.projectCode || NS.config.projectName
105                      || NS.config.shortProjectName)
106                     : false)
107                        || window.location.pathname
108                )+'::' : (
109                    '' /* transient storage */
110                )
111        );
112
113        /**
114           A proxy for localStorage or sessionStorage or a
115           page-instance-local proxy, if neither one is availble.
116
117           Which exact storage implementation is uses is unspecified, and
118           apps must not rely on it.
119        */
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));
138            },
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);
146                }
147                catch(e){return dflt}
148            },
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;
156            },
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;
161            },
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';
173            },
174
175            /**
176               Returns a brief help text string for the currently-selected
177               storage type.
178            */
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()];
188            }
189        };
190        return NS.storage;
191    })({})/*storage API setup*/;
192
193
194    /** Name of the stored copy of SqliteFiddle.config. */
195    const configStorageKey = 'sqlite3-fiddle-config';
196
197    /**
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.
202    */
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
222        },
223        /**
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).
230        */
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
240            }
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 = '';
254            }
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;
260            }
261        },
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;
269            }
270            (this._msgMap.hasOwnProperty(type)
271             ? this._msgMap[type]
272             : (this._msgMap[type] = [])).push(callback);
273            return this;
274        },
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;
282            }
283            //console.debug("runMsgHandlers",msg);
284            list.forEach((f)=>f(msg));
285            return true;
286        },
287        /** Removes all message handlers for the given message type. */
288        clearMsgHandlers: function(type){
289            delete this._msgMap[type];
290            return this;
291        },
292        /* Posts a message in the form {type, data} to the db worker. Returns this. */
293        wMsg: function(type,data){
294            this.worker.postMessage({type, data});
295            return this;
296        },
297        /**
298           Prompts for confirmation and, if accepted, deletes
299           all content and tables in the (transient) database.
300        */
301        resetDb: function(){
302            if(window.confirm("Really destroy all content and tables "
303                              +"in the (transient) db?")){
304                this.wMsg('db-reset');
305            }
306            return this;
307        },
308        /** Stores this object's config in the browser's storage. */
309        storeConfig: function(){
310            storage.setJSON(configStorageKey,this.config);
311        }
312    };
313
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];
324                }
325            });
326        }
327    }
328
329    SF.worker = new Worker('fiddle-worker.js');
330    SF.worker.onmessage = (ev)=>SF.runMsgHandlers(ev.data);
331    SF.addMsgHandler(['stdout', 'stderr'], (ev)=>SF.echo(ev.data));
332
333    /* querySelectorAll() proxy */
334    const EAll = function(/*[element=document,] cssSelector*/){
335        return (arguments.length>1 ? arguments[0] : document)
336            .querySelectorAll(arguments[arguments.length-1]);
337    };
338    /* querySelector() proxy */
339    const E = function(/*[element=document,] cssSelector*/){
340        return (arguments.length>1 ? arguments[0] : document)
341            .querySelector(arguments[arguments.length-1]);
342    };
343
344    /** Handles status updates from the 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;
350        }
351        if(!f.ui){
352            f.ui = {
353                status: E('#module-status'),
354                progress: E('#module-progress'),
355                spinner: E('#module-spinner')
356            };
357        }
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*/;
362        }
363        if(1==msg.step){
364            f.ui.progress.classList.remove('hidden');
365            f.ui.spinner.classList.remove('hidden');
366        }
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;
376            }
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. */
382        }
383    });
384
385    /**
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();
392    });
393
394    /**
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.
398    */
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        btnClearIn.addEventListener('click',function(){
407            taInput.value = '';
408        },false);
409        // Ctrl-enter and shift-enter both run the current SQL.
410        taInput.addEventListener('keydown',function(ev){
411            if((ev.ctrlKey || ev.shiftKey) && 13 === ev.keyCode){
412                ev.preventDefault();
413                ev.stopPropagation();
414                btnShellExec.click();
415            }
416        }, false);
417        const taOutput = E('#output');
418        const btnClearOut = E('#btn-clear-output');
419        btnClearOut.addEventListener('click',function(){
420            taOutput.value = '';
421            if(SF.jqTerm) SF.jqTerm.clear();
422        },false);
423        const btnShellExec = E('#btn-shell-exec');
424        btnShellExec.addEventListener('click',function(ev){
425            let sql;
426            ev.preventDefault();
427            if(taInput.selectionStart<taInput.selectionEnd){
428                sql = taInput.value.substring(taInput.selectionStart,taInput.selectionEnd).trim();
429            }else{
430                sql = taInput.value.trim();
431            }
432            if(sql) SF.dbExec(sql);
433        },false);
434
435        const btnInterrupt = E("#btn-interrupt");
436        //btnInterrupt.classList.add('hidden');
437        /** To be called immediately before work is sent to the
438            worker. Updates some UI elements. The 'working'/'end'
439            event will apply the inverse, undoing the bits this
440            function does. This impl is not in the 'working'/'start'
441            event handler because that event is given to us
442            asynchronously _after_ we need to have performed this
443            work.
444        */
445        const preStartWork = function f(){
446            if(!f._){
447                const title = E('title');
448                f._ = {
449                    btnLabel: btnShellExec.innerText,
450                    pageTitle: title,
451                    pageTitleOrig: title.innerText
452                };
453            }
454            f._.pageTitle.innerText = "[working...] "+f._.pageTitleOrig;
455            btnShellExec.setAttribute('disabled','disabled');
456            btnInterrupt.removeAttribute('disabled','disabled');
457        };
458
459        /* Sends the given text to the db module to evaluate as if it
460           had been entered in the sqlite3 CLI shell. If it's null or
461           empty, this is a no-op except that the very first call will
462           initialize the db and output an informational header. */
463        SF.dbExec = function f(sql){
464            if(this.config.autoClearOutput){
465                this.echo._clearPending = true;
466            }
467            preStartWork();
468            this.wMsg('shellExec',sql);
469        };
470
471        SF.addMsgHandler('working',function f(ev){
472            switch(ev.data){
473                case 'start': /* See notes in preStartWork(). */; return;
474                case 'end':
475                    preStartWork._.pageTitle.innerText = preStartWork._.pageTitleOrig;
476                    btnShellExec.innerText = preStartWork._.btnLabel;
477                    btnShellExec.removeAttribute('disabled');
478                    btnInterrupt.setAttribute('disabled','disabled');
479                    return;
480            }
481            console.warn("Unhandled 'working' event:",ev.data);
482        });
483
484        /* For each checkbox with data-csstgt, set up a handler which
485           toggles the given CSS class on the element matching
486           E(data-csstgt). */
487        EAll('input[type=checkbox][data-csstgt]')
488            .forEach(function(e){
489                const tgt = E(e.dataset.csstgt);
490                const cssClass = e.dataset.cssclass || 'error';
491                e.checked = tgt.classList.contains(cssClass);
492                e.addEventListener('change', function(){
493                    tgt.classList[
494                        this.checked ? 'add' : 'remove'
495                    ](cssClass)
496                }, false);
497            });
498        /* For each checkbox with data-config=X, set up a binding to
499           SF.config[X]. These must be set up AFTER data-csstgt
500           checkboxes so that those two states can be synced properly. */
501        EAll('input[type=checkbox][data-config]')
502            .forEach(function(e){
503                const confVal = !!SF.config[e.dataset.config];
504                if(e.checked !== confVal){
505                    /* Ensure that data-csstgt mappings (if any) get
506                       synced properly. */
507                    e.checked = confVal;
508                    e.dispatchEvent(new Event('change'));
509                }
510                e.addEventListener('change', function(){
511                    SF.config[this.dataset.config] = this.checked;
512                    SF.storeConfig();
513                }, false);
514            });
515        /* For each button with data-cmd=X, map a click handler which
516           calls SF.dbExec(X). */
517        const cmdClick = function(){SF.dbExec(this.dataset.cmd);};
518        EAll('button[data-cmd]').forEach(
519            e => e.addEventListener('click', cmdClick, false)
520        );
521
522        btnInterrupt.addEventListener('click',function(){
523            SF.wMsg('interrupt');
524        });
525
526        /** Initiate a download of the db. */
527        const btnExport = E('#btn-export');
528        const eLoadDb = E('#load-db');
529        const btnLoadDb = E('#btn-load-db');
530        btnLoadDb.addEventListener('click', ()=>eLoadDb.click());
531        /**
532           Enables (if passed true) or disables all UI elements which
533           "might," if timed "just right," interfere with an
534           in-progress db import/export/exec operation.
535        */
536        const enableMutatingElements = function f(enable){
537            if(!f._elems){
538                f._elems = [
539                    /* UI elements to disable while import/export are
540                       running. Normally the export is fast enough
541                       that this won't matter, but we really don't
542                       want to be reading (from outside of sqlite) the
543                       db when the user taps btnShellExec. */
544                    btnShellExec, btnExport, eLoadDb
545                ];
546            }
547            f._elems.forEach( enable
548                              ? (e)=>e.removeAttribute('disabled')
549                              : (e)=>e.setAttribute('disabled','disabled') );
550        };
551        btnExport.addEventListener('click',function(){
552            enableMutatingElements(false);
553            SF.wMsg('db-export');
554        });
555        SF.addMsgHandler('db-export', function(ev){
556            enableMutatingElements(true);
557            ev = ev.data;
558            if(ev.error){
559                SF.echo("Export failed:",ev.error);
560                return;
561            }
562            const blob = new Blob([ev.buffer], {type:"application/x-sqlite3"});
563            const a = document.createElement('a');
564            document.body.appendChild(a);
565            a.href = window.URL.createObjectURL(blob);
566            a.download = ev.filename;
567            a.addEventListener('click',function(){
568                setTimeout(function(){
569                    SF.echo("Exported (possibly auto-downloaded):",ev.filename);
570                    window.URL.revokeObjectURL(a.href);
571                    a.remove();
572                },500);
573            });
574            a.click();
575        });
576        /**
577           Handle load/import of an external db file.
578        */
579        eLoadDb.addEventListener('change',function(){
580            const f = this.files[0];
581            const r = new FileReader();
582            const status = {loaded: 0, total: 0};
583            enableMutatingElements(false);
584            r.addEventListener('loadstart', function(){
585                SF.echo("Loading",f.name,"...");
586            });
587            r.addEventListener('progress', function(ev){
588                SF.echo("Loading progress:",ev.loaded,"of",ev.total,"bytes.");
589            });
590            const that = this;
591            r.addEventListener('load', function(){
592                enableMutatingElements(true);
593                SF.echo("Loaded",f.name+". Opening db...");
594                SF.wMsg('open',{
595                    filename: f.name,
596                    buffer: this.result
597                });
598            });
599            r.addEventListener('error',function(){
600                enableMutatingElements(true);
601                SF.echo("Loading",f.name,"failed for unknown reasons.");
602            });
603            r.addEventListener('abort',function(){
604                enableMutatingElements(true);
605                SF.echo("Cancelled loading of",f.name+".");
606            });
607            r.readAsArrayBuffer(f);
608        });
609
610        EAll('fieldset.collapsible').forEach(function(fs){
611            const btnToggle = E(fs,'legend > .fieldset-toggle'),
612                  content = EAll(fs,':scope > div');
613            btnToggle.addEventListener('click', function(){
614                fs.classList.toggle('collapsed');
615                content.forEach((d)=>d.classList.toggle('hidden'));
616            }, false);
617        });
618
619        /**
620           Given a DOM element, this routine measures its "effective
621           height", which is the bounding top/bottom range of this element
622           and all of its children, recursively. For some DOM structure
623           cases, a parent may have a reported height of 0 even though
624           children have non-0 sizes.
625
626           Returns 0 if !e or if the element really has no height.
627        */
628        const effectiveHeight = function f(e){
629            if(!e) return 0;
630            if(!f.measure){
631                f.measure = function callee(e, depth){
632                    if(!e) return;
633                    const m = e.getBoundingClientRect();
634                    if(0===depth){
635                        callee.top = m.top;
636                        callee.bottom = m.bottom;
637                    }else{
638                        callee.top = m.top ? Math.min(callee.top, m.top) : callee.top;
639                        callee.bottom = Math.max(callee.bottom, m.bottom);
640                    }
641                    Array.prototype.forEach.call(e.children,(e)=>callee(e,depth+1));
642                    if(0===depth){
643                        //console.debug("measure() height:",e.className, callee.top, callee.bottom, (callee.bottom - callee.top));
644                        f.extra += callee.bottom - callee.top;
645                    }
646                    return f.extra;
647                };
648            }
649            f.extra = 0;
650            f.measure(e,0);
651            return f.extra;
652        };
653
654        /**
655           Returns a function, that, as long as it continues to be invoked,
656           will not be triggered. The function will be called after it stops
657           being called for N milliseconds. If `immediate` is passed, call
658           the callback immediately and hinder future invocations until at
659           least the given time has passed.
660
661           If passed only 1 argument, or passed a falsy 2nd argument,
662           the default wait time set in this function's $defaultDelay
663           property is used.
664
665           Source: underscore.js, by way of https://davidwalsh.name/javascript-debounce-function
666        */
667        const debounce = function f(func, wait, immediate) {
668            var timeout;
669            if(!wait) wait = f.$defaultDelay;
670            return function() {
671                const context = this, args = Array.prototype.slice.call(arguments);
672                const later = function() {
673                    timeout = undefined;
674                    if(!immediate) func.apply(context, args);
675                };
676                const callNow = immediate && !timeout;
677                clearTimeout(timeout);
678                timeout = setTimeout(later, wait);
679                if(callNow) func.apply(context, args);
680            };
681        };
682        debounce.$defaultDelay = 500 /*arbitrary*/;
683
684        const ForceResizeKludge = (function(){
685            /* Workaround for Safari mayhem regarding use of vh CSS
686               units....  We cannot use vh units to set the main view
687               size because Safari chokes on that, so we calculate
688               that height here. Larger than ~95% is too big for
689               Firefox on Android, causing the input area to move
690               off-screen. */
691            const appViews = EAll('.app-view');
692            const elemsToCount = [
693                /* Elements which we need to always count in the
694                   visible body size. */
695                E('body > header'),
696                E('body > footer')
697            ];
698            const resized = function f(){
699                if(f.$disabled) return;
700                const wh = window.innerHeight;
701                var ht;
702                var extra = 0;
703                elemsToCount.forEach((e)=>e ? extra += effectiveHeight(e) : false);
704                ht = wh - extra;
705                appViews.forEach(function(e){
706                    e.style.height =
707                        e.style.maxHeight = [
708                            "calc(", (ht>=100 ? ht : 100), "px",
709                            " - 2em"/*fudge value*/,")"
710                            /* ^^^^ hypothetically not needed, but both
711                               Chrome/FF on Linux will force scrollbars on the
712                               body if this value is too small. */
713                        ].join('');
714                });
715            };
716            resized.$disabled = true/*gets deleted when setup is finished*/;
717            window.addEventListener('resize', debounce(resized, 250), false);
718            return resized;
719        })();
720
721        /** Set up a selection list of examples */
722        (function(){
723            const xElem = E('#select-examples');
724            const examples = [
725                {name: "Help", sql:
726`-- ================================================
727-- Use ctrl-enter or shift-enter to execute sqlite3
728-- shell commands and SQL.
729-- If a subset of the text is currently selected,
730-- only that part is executed.
731-- ================================================
732.help`},
733                {name: "Timer on", sql: ".timer on"},
734                {name: "Setup table T", sql:`.nullvalue NULL
735CREATE TABLE t(a,b);
736INSERT INTO t(a,b) VALUES('abc',123),('def',456),(NULL,789),('ghi',012);
737SELECT * FROM t;`},
738                {name: "Table list", sql: ".tables"},
739                {name: "Box Mode", sql: ".mode box"},
740                {name: "JSON Mode", sql: ".mode json"},
741                {name: "Mandlebrot", sql: `WITH RECURSIVE
742  xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2),
743  yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0),
744  m(iter, cx, cy, x, y) AS (
745    SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis
746    UNION ALL
747    SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m
748     WHERE (x*x + y*y) < 4.0 AND iter<28
749  ),
750  m2(iter, cx, cy) AS (
751    SELECT max(iter), cx, cy FROM m GROUP BY cx, cy
752  ),
753  a(t) AS (
754    SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '')
755    FROM m2 GROUP BY cy
756  )
757SELECT group_concat(rtrim(t),x'0a') as Mandelbrot FROM a;`}
758            ];
759            const newOpt = function(lbl,val){
760                const o = document.createElement('option');
761                o.value = val;
762                if(!val) o.setAttribute('disabled',true);
763                o.appendChild(document.createTextNode(lbl));
764                xElem.appendChild(o);
765            };
766            newOpt("Examples (replaces input!)");
767            examples.forEach((o)=>newOpt(o.name, o.sql));
768            //xElem.setAttribute('disabled',true);
769            xElem.selectedIndex = 0;
770            xElem.addEventListener('change', function(){
771                taInput.value = '-- ' +
772                    this.selectedOptions[0].innerText +
773                    '\n' + this.value;
774                SF.dbExec(this.value);
775            });
776        })()/* example queries */;
777
778        //SF.echo(null/*clear any output generated by the init process*/);
779        if(window.jQuery && window.jQuery.terminal){
780            /* Set up the terminal-style view... */
781            const eTerm = window.jQuery('#view-terminal').empty();
782            SF.jqTerm = eTerm.terminal(SF.dbExec.bind(SF),{
783                prompt: 'sqlite> ',
784                greetings: false /* note that the docs incorrectly call this 'greeting' */
785            });
786            /* Set up a button to toggle the views... */
787            const head = E('header#titlebar');
788            const btnToggleView = document.createElement('button');
789            btnToggleView.appendChild(document.createTextNode("Toggle View"));
790            head.appendChild(btnToggleView);
791            btnToggleView.addEventListener('click',function f(){
792                EAll('.app-view').forEach(e=>e.classList.toggle('hidden'));
793                if(document.body.classList.toggle('terminal-mode')){
794                    ForceResizeKludge();
795                }
796            }, false);
797            btnToggleView.click()/*default to terminal view*/;
798        }
799        SF.dbExec(null/*init the db and output the header*/);
800        SF.echo('This experimental app is provided in the hope that it',
801                'may prove interesting or useful but is not an officially',
802                'supported deliverable of the sqlite project. It is subject to',
803                'any number of changes or outright removal at any time.\n');
804        delete ForceResizeKludge.$disabled;
805        ForceResizeKludge();
806
807        btnShellExec.click();
808    }/*onSFLoaded()*/;
809})();
810