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, "<"); 246 //text = text.replace(/>/g, ">"); 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