xref: /sqlite-3.40.0/ext/wasm/fiddle/fiddle.js (revision eb97743c)
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,transferables){
294            this.worker.postMessage({type, data}, transferables || []);
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'+self.location.search);
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 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;
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. */
462    SF.dbExec = function f(sql){
463      if(null!==sql && this.config.autoClearOutput){
464        this.echo._clearPending = true;
465      }
466      preStartWork();
467      this.wMsg('shellExec',sql);
468    };
469
470    SF.addMsgHandler('working',function f(ev){
471      switch(ev.data){
472          case 'start': /* See notes in preStartWork(). */; return;
473          case 'end':
474            preStartWork._.pageTitle.innerText = preStartWork._.pageTitleOrig;
475            btnShellExec.innerText = preStartWork._.btnLabel;
476            btnShellExec.removeAttribute('disabled');
477            btnInterrupt.setAttribute('disabled','disabled');
478            return;
479      }
480      console.warn("Unhandled 'working' event:",ev.data);
481    });
482
483    /* For each checkbox with data-csstgt, set up a handler which
484       toggles the given CSS class on the element matching
485       E(data-csstgt). */
486    EAll('input[type=checkbox][data-csstgt]')
487      .forEach(function(e){
488        const tgt = E(e.dataset.csstgt);
489        const cssClass = e.dataset.cssclass || 'error';
490        e.checked = tgt.classList.contains(cssClass);
491        e.addEventListener('change', function(){
492          tgt.classList[
493            this.checked ? 'add' : 'remove'
494          ](cssClass)
495        }, false);
496      });
497    /* For each checkbox with data-config=X, set up a binding to
498       SF.config[X]. These must be set up AFTER data-csstgt
499       checkboxes so that those two states can be synced properly. */
500    EAll('input[type=checkbox][data-config]')
501      .forEach(function(e){
502        const confVal = !!SF.config[e.dataset.config];
503        if(e.checked !== confVal){
504          /* Ensure that data-csstgt mappings (if any) get
505             synced properly. */
506          e.checked = confVal;
507          e.dispatchEvent(new Event('change'));
508        }
509        e.addEventListener('change', function(){
510          SF.config[this.dataset.config] = this.checked;
511          SF.storeConfig();
512        }, false);
513      });
514    /* For each button with data-cmd=X, map a click handler which
515       calls SF.dbExec(X). */
516    const cmdClick = function(){SF.dbExec(this.dataset.cmd);};
517    EAll('button[data-cmd]').forEach(
518      e => e.addEventListener('click', cmdClick, false)
519    );
520
521    btnInterrupt.addEventListener('click',function(){
522      SF.wMsg('interrupt');
523    });
524
525    /** Initiate a download of the db. */
526    const btnExport = E('#btn-export');
527    const eLoadDb = E('#load-db');
528    const btnLoadDb = E('#btn-load-db');
529    btnLoadDb.addEventListener('click', ()=>eLoadDb.click());
530    /**
531       Enables (if passed true) or disables all UI elements which
532       "might," if timed "just right," interfere with an
533       in-progress db import/export/exec operation.
534    */
535    const enableMutatingElements = function f(enable){
536      if(!f._elems){
537        f._elems = [
538          /* UI elements to disable while import/export are
539             running. Normally the export is fast enough
540             that this won't matter, but we really don't
541             want to be reading (from outside of sqlite) the
542             db when the user taps btnShellExec. */
543          btnShellExec, btnExport, eLoadDb
544        ];
545      }
546      f._elems.forEach( enable
547                        ? (e)=>e.removeAttribute('disabled')
548                        : (e)=>e.setAttribute('disabled','disabled') );
549    };
550    btnExport.addEventListener('click',function(){
551      enableMutatingElements(false);
552      SF.wMsg('db-export');
553    });
554    SF.addMsgHandler('db-export', function(ev){
555      enableMutatingElements(true);
556      ev = ev.data;
557      if(ev.error){
558        SF.echo("Export failed:",ev.error);
559        return;
560      }
561      const blob = new Blob([ev.buffer],
562                            {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        }, [this.result]);
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          "-- ================================================\n",
727          "-- Use ctrl-enter or shift-enter to execute sqlite3\n",
728          "-- shell commands and SQL.\n",
729          "-- If a subset of the text is currently selected,\n",
730          "-- only that part is executed.\n",
731          "-- ================================================\n",
732          ".help\n"
733        ]},
734              //{name: "Timer on", sql: ".timer on"},
735              // ^^^ re-enable if emscripten re-enables getrusage()
736        {name: "Setup table T", sql:[
737          ".nullvalue NULL\n",
738          "CREATE TABLE t(a,b);\n",
739          "INSERT INTO t(a,b) VALUES('abc',123),('def',456),(NULL,789),('ghi',012);\n",
740          "SELECT * FROM t;\n"
741        ]},
742        {name: "Table list", sql: ".tables"},
743        {name: "Box Mode", sql: ".mode box"},
744        {name: "JSON Mode", sql: ".mode json"},
745        {name: "Mandlebrot", sql:[
746          "WITH RECURSIVE",
747          "  xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2),\n",
748          "  yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0),\n",
749          "  m(iter, cx, cy, x, y) AS (\n",
750          "    SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis\n",
751          "    UNION ALL\n",
752          "    SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m \n",
753          "     WHERE (x*x + y*y) < 4.0 AND iter<28\n",
754          "  ),\n",
755          "  m2(iter, cx, cy) AS (\n",
756          "    SELECT max(iter), cx, cy FROM m GROUP BY cx, cy\n",
757          "  ),\n",
758          "  a(t) AS (\n",
759          "    SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '') \n",
760          "    FROM m2 GROUP BY cy\n",
761          "  )\n",
762          "SELECT group_concat(rtrim(t),x'0a') as Mandelbrot FROM a;\n",
763        ]}
764      ];
765      const newOpt = function(lbl,val){
766        const o = document.createElement('option');
767        if(Array.isArray(val)) val = val.join('');
768        o.value = val;
769        if(!val) o.setAttribute('disabled',true);
770        o.appendChild(document.createTextNode(lbl));
771        xElem.appendChild(o);
772      };
773      newOpt("Examples (replaces input!)");
774      examples.forEach((o)=>newOpt(o.name, o.sql));
775      //xElem.setAttribute('disabled',true);
776      xElem.selectedIndex = 0;
777      xElem.addEventListener('change', function(){
778        taInput.value = '-- ' +
779          this.selectedOptions[0].innerText +
780          '\n' + this.value;
781        SF.dbExec(this.value);
782      });
783    })()/* example queries */;
784
785    //SF.echo(null/*clear any output generated by the init process*/);
786    if(window.jQuery && window.jQuery.terminal){
787      /* Set up the terminal-style view... */
788      const eTerm = window.jQuery('#view-terminal').empty();
789      SF.jqTerm = eTerm.terminal(SF.dbExec.bind(SF),{
790        prompt: 'sqlite> ',
791        greetings: false /* note that the docs incorrectly call this 'greeting' */
792      });
793      /* Set up a button to toggle the views... */
794      const head = E('header#titlebar');
795      const btnToggleView = document.createElement('button');
796      btnToggleView.appendChild(document.createTextNode("Toggle View"));
797      head.appendChild(btnToggleView);
798      btnToggleView.addEventListener('click',function f(){
799        EAll('.app-view').forEach(e=>e.classList.toggle('hidden'));
800        if(document.body.classList.toggle('terminal-mode')){
801          ForceResizeKludge();
802        }
803      }, false);
804      btnToggleView.click()/*default to terminal view*/;
805    }
806    SF.echo('This experimental app is provided in the hope that it',
807            'may prove interesting or useful but is not an officially',
808            'supported deliverable of the sqlite project. It is subject to',
809            'any number of changes or outright removal at any time.\n');
810    const urlParams = new URL(self.location.href).searchParams;
811    SF.dbExec(urlParams.get('sql') || null);
812    delete ForceResizeKludge.$disabled;
813    ForceResizeKludge();
814  }/*onSFLoaded()*/;
815})();
816