fiddle.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. /*
  2. 2022-05-20
  3. The author disclaims copyright to this source code. In place of a
  4. legal notice, here is a blessing:
  5. * May you do good and not evil.
  6. * May you find forgiveness for yourself and forgive others.
  7. * May you share freely, never taking more than you give.
  8. ***********************************************************************
  9. This is the main entry point for the pikchr fiddle app. It sets up the
  10. various UI bits, loads a Worker for the pikchr process, and manages the
  11. communication between the UI and worker.
  12. */
  13. (function(){
  14. 'use strict';
  15. /* Recall that the 'self' symbol, except where locally
  16. overwritten, refers to the global window or worker object. */
  17. const storage = (function(NS/*namespace object in which to store this module*/){
  18. /* Pedantic licensing note: this code originated in the Fossil SCM
  19. source tree, where it has a different license, but the person who
  20. ported it into here is the same one who wrote it for fossil. */
  21. 'use strict';
  22. NS = NS||{};
  23. /**
  24. This module provides a basic wrapper around localStorage
  25. or sessionStorage or a dummy proxy object if neither
  26. of those are available.
  27. */
  28. const tryStorage = function f(obj){
  29. if(!f.key) f.key = 'storage.access.check';
  30. try{
  31. obj.setItem(f.key, 'f');
  32. const x = obj.getItem(f.key);
  33. obj.removeItem(f.key);
  34. if(x!=='f') throw new Error(f.key+" failed")
  35. return obj;
  36. }catch(e){
  37. return undefined;
  38. }
  39. };
  40. /** Internal storage impl for this module. */
  41. const $storage =
  42. tryStorage(window.localStorage)
  43. || tryStorage(window.sessionStorage)
  44. || tryStorage({
  45. // A basic dummy xyzStorage stand-in
  46. $$$:{},
  47. setItem: function(k,v){this.$$$[k]=v},
  48. getItem: function(k){
  49. return this.$$$.hasOwnProperty(k) ? this.$$$[k] : undefined;
  50. },
  51. removeItem: function(k){delete this.$$$[k]},
  52. clear: function(){this.$$$={}}
  53. });
  54. /**
  55. For the dummy storage we need to differentiate between
  56. $storage and its real property storage for hasOwnProperty()
  57. to work properly...
  58. */
  59. const $storageHolder = $storage.hasOwnProperty('$$$') ? $storage.$$$ : $storage;
  60. /**
  61. A prefix which gets internally applied to all storage module
  62. property keys so that localStorage and sessionStorage across the
  63. same browser profile instance do not "leak" across multiple apps
  64. being hosted by the same origin server. Such cross-polination is
  65. still there but, with this key prefix applied, it won't be
  66. immediately visible via the storage API.
  67. With this in place we can justify using localStorage instead of
  68. sessionStorage.
  69. One implication of using localStorage and sessionStorage is that
  70. their scope (the same "origin" and client application/profile)
  71. allows multiple apps on the same origin to use the same
  72. storage. Thus /appA/foo could then see changes made via
  73. /appB/foo. The data do not cross user- or browser boundaries,
  74. though, so it "might" arguably be called a
  75. feature. storageKeyPrefix was added so that we can sandbox that
  76. state for each separate app which shares an origin.
  77. See: https://fossil-scm.org/forum/forumpost/4afc4d34de
  78. Sidebar: it might seem odd to provide a key prefix and stick all
  79. properties in the topmost level of the storage object. We do that
  80. because adding a layer of object to sandbox each app would mean
  81. (de)serializing that whole tree on every storage property change.
  82. e.g. instead of storageObject.projectName.foo we have
  83. storageObject[storageKeyPrefix+'foo']. That's soley for
  84. efficiency's sake (in terms of battery life and
  85. environment-internal storage-level effort).
  86. */
  87. const storageKeyPrefix = (
  88. $storageHolder===$storage/*localStorage or sessionStorage*/
  89. ? (
  90. (NS.config ?
  91. (NS.config.projectCode || NS.config.projectName
  92. || NS.config.shortProjectName)
  93. : false)
  94. || window.location.pathname
  95. )+'::' : (
  96. '' /* transient storage */
  97. )
  98. );
  99. /**
  100. A proxy for localStorage or sessionStorage or a
  101. page-instance-local proxy, if neither one is availble.
  102. Which exact storage implementation is uses is unspecified, and
  103. apps must not rely on it.
  104. */
  105. NS.storage = {
  106. storageKeyPrefix: storageKeyPrefix,
  107. /** Sets the storage key k to value v, implicitly converting
  108. it to a string. */
  109. set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v),
  110. /** Sets storage key k to JSON.stringify(v). */
  111. setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
  112. /** Returns the value for the given storage key, or
  113. dflt if the key is not found in the storage. */
  114. get: (k,dflt)=>$storageHolder.hasOwnProperty(
  115. storageKeyPrefix+k
  116. ) ? $storage.getItem(storageKeyPrefix+k) : dflt,
  117. /** Returns true if the given key has a value of "true". If the
  118. key is not found, it returns true if the boolean value of dflt
  119. is "true". (Remember that JS persistent storage values are all
  120. strings.) */
  121. getBool: function(k,dflt){
  122. return 'true'===this.get(k,''+(!!dflt));
  123. },
  124. /** Returns the JSON.parse()'d value of the given
  125. storage key's value, or dflt is the key is not
  126. found or JSON.parse() fails. */
  127. getJSON: function f(k,dflt){
  128. try {
  129. const x = this.get(k,f);
  130. return x===f ? dflt : JSON.parse(x);
  131. }
  132. catch(e){return dflt}
  133. },
  134. /** Returns true if the storage contains the given key,
  135. else false. */
  136. contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k),
  137. /** Removes the given key from the storage. Returns this. */
  138. remove: function(k){
  139. $storage.removeItem(storageKeyPrefix+k);
  140. return this;
  141. },
  142. /** Clears ALL keys from the storage. Returns this. */
  143. clear: function(){
  144. this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k));
  145. return this;
  146. },
  147. /** Returns an array of all keys currently in the storage. */
  148. keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
  149. /** Returns true if this storage is transient (only available
  150. until the page is reloaded), indicating that fileStorage
  151. and sessionStorage are unavailable. */
  152. isTransient: ()=>$storageHolder!==$storage,
  153. /** Returns a symbolic name for the current storage mechanism. */
  154. storageImplName: function(){
  155. if($storage===window.localStorage) return 'localStorage';
  156. else if($storage===window.sessionStorage) return 'sessionStorage';
  157. else return 'transient';
  158. },
  159. /**
  160. Returns a brief help text string for the currently-selected
  161. storage type.
  162. */
  163. storageHelpDescription: function(){
  164. return {
  165. localStorage: "Browser-local persistent storage with an "+
  166. "unspecified long-term lifetime (survives closing the browser, "+
  167. "but maybe not a browser upgrade).",
  168. sessionStorage: "Storage local to this browser tab, "+
  169. "lost if this tab is closed.",
  170. "transient": "Transient storage local to this invocation of this page."
  171. }[this.storageImplName()];
  172. }
  173. };
  174. return NS.storage;
  175. })({})/*storage API setup*/;
  176. /** Name of the stored copy of PikchrFiddle.config. */
  177. const configStorageKey = 'pikchr-fiddle-config';
  178. /**
  179. The PikchrFiddle object is intended to be the primary app-level
  180. object for the main-thread side of the fiddle application. It
  181. uses a worker thread to load the WASM module and communicate
  182. with it.
  183. */
  184. const PF/*local convenience alias*/
  185. = self.PikchrFiddle/*canonical name*/ = {
  186. /* Config options. */
  187. config: {
  188. /* If true, display input/output areas side-by-side. */
  189. sideBySide: true,
  190. /* If true, swap positions of the input/output areas. */
  191. swapInOut: true,
  192. /* If true, the SVG is allowed to resize to fit the
  193. parent content area, else the parent is resized to
  194. fit the rendered SVG. */
  195. renderAutoScale: false,
  196. /* If true, automatically render while the user is
  197. typing. */
  198. renderWhileTyping: false
  199. },
  200. renderMode: 'html'/*one of: 'text','html'*/,
  201. _msgMap: {},
  202. /** Adds a worker message handler for messages of the given
  203. type. */
  204. addMsgHandler: function f(type,callback){
  205. if(Array.isArray(type)){
  206. type.forEach((t)=>this.addMsgHandler(t, callback));
  207. return this;
  208. }
  209. (this._msgMap.hasOwnProperty(type)
  210. ? this._msgMap[type]
  211. : (this._msgMap[type] = [])).push(callback);
  212. return this;
  213. },
  214. /** Given a worker message, runs all handlers for msg.type. */
  215. runMsgHandlers: function(msg){
  216. const list = (this._msgMap.hasOwnProperty(msg.type)
  217. ? this._msgMap[msg.type] : false);
  218. if(!list){
  219. console.warn("No handlers found for message type:",msg);
  220. return false;
  221. }
  222. list.forEach((f)=>f(msg));
  223. return true;
  224. },
  225. /** Removes all message handlers for the given message type. */
  226. clearMsgHandlers: function(type){
  227. delete this._msgMap[type];
  228. return this;
  229. },
  230. /* Posts a message in the form {type, data} to the db worker. Returns this. */
  231. wMsg: function(type,data){
  232. this.worker.postMessage({type, data});
  233. return this;
  234. },
  235. /** Stores this object's config in the browser's storage. */
  236. storeConfig: function(){
  237. storage.setJSON(configStorageKey,this.config);
  238. }
  239. };
  240. if(1){ /* set up PF.config */
  241. const storedConfig = storage.getJSON(configStorageKey);
  242. if(storedConfig){
  243. /* Copy all properties to PF.config which are currently in
  244. storedConfig. We don't bother copying any other
  245. properties: those have been removed from the app in the
  246. meantime. */
  247. Object.keys(PF.config).forEach(function(k){
  248. if(storedConfig.hasOwnProperty(k)){
  249. PF.config[k] = storedConfig[k];
  250. }
  251. });
  252. }
  253. }
  254. PF.worker = new Worker('pikchr-worker.js');
  255. PF.worker.onmessage = (ev)=>PF.runMsgHandlers(ev.data);
  256. PF.addMsgHandler('stdout', console.log.bind(console));
  257. PF.addMsgHandler('stderr', console.error.bind(console));
  258. /* querySelectorAll() proxy */
  259. const EAll = function(/*[element=document,] cssSelector*/){
  260. return (arguments.length>1 ? arguments[0] : document)
  261. .querySelectorAll(arguments[arguments.length-1]);
  262. };
  263. /* querySelector() proxy */
  264. const E = function(/*[element=document,] cssSelector*/){
  265. return (arguments.length>1 ? arguments[0] : document)
  266. .querySelector(arguments[arguments.length-1]);
  267. };
  268. /** Handles status updates from the Module object. */
  269. PF.addMsgHandler('module', function f(ev){
  270. ev = ev.data;
  271. if('status'!==ev.type){
  272. console.warn("Unexpected module-type message:",ev);
  273. return;
  274. }
  275. if(!f.ui){
  276. f.ui = {
  277. status: E('#module-status'),
  278. progress: E('#module-progress'),
  279. spinner: E('#module-spinner')
  280. };
  281. }
  282. const msg = ev.data;
  283. if(f.ui.progres){
  284. progress.value = msg.step;
  285. progress.max = msg.step + 1/*we don't know how many steps to expect*/;
  286. }
  287. if(1==msg.step){
  288. f.ui.progress.classList.remove('hidden');
  289. f.ui.spinner.classList.remove('hidden');
  290. }
  291. if(msg.text){
  292. f.ui.status.classList.remove('hidden');
  293. f.ui.status.innerText = msg.text;
  294. }else{
  295. if(f.ui.progress){
  296. f.ui.progress.remove();
  297. f.ui.spinner.remove();
  298. delete f.ui.progress;
  299. delete f.ui.spinner;
  300. }
  301. f.ui.status.classList.add('hidden');
  302. /* The module can post messages about fatal problems,
  303. e.g. an exit() being triggered or assertion failure,
  304. after the last "load" message has arrived, so
  305. leave f.ui.status and message listener intact. */
  306. }
  307. });
  308. /**
  309. The 'pikchr-ready' event is fired (with no payload) when the
  310. wasm module has finished loading. Interestingly, that happens
  311. _before_ the final module:status event */
  312. PF.addMsgHandler('pikchr-ready', function(){
  313. PF.clearMsgHandlers('pikchr-ready');
  314. self.onPFLoaded();
  315. });
  316. /**
  317. Performs all app initialization which must wait until after the
  318. worker module is loaded. This function removes itself when it's
  319. called.
  320. */
  321. self.onPFLoaded = function(){
  322. delete this.onPFLoaded;
  323. // Unhide all elements which start out hidden
  324. EAll('.initially-hidden').forEach((e)=>e.classList.remove('initially-hidden'));
  325. const taInput = E('#input');
  326. const btnClearIn = E('#btn-clear');
  327. btnClearIn.addEventListener('click',function(){
  328. taInput.value = '';
  329. },false);
  330. const taOutput = E('#output');
  331. const btnRender = E('#btn-render');
  332. const getCurrentText = function(){
  333. let text;
  334. if(taInput.selectionStart<taInput.selectionEnd){
  335. text = taInput.value.substring(taInput.selectionStart,taInput.selectionEnd).trim();
  336. }else{
  337. text = taInput.value.trim();
  338. }
  339. return text;;
  340. }
  341. const renderCurrentText = function(){
  342. const text = getCurrentText();
  343. if(text) PF.render(text);
  344. };
  345. btnRender.addEventListener('click',function(ev){
  346. ev.preventDefault();
  347. renderCurrentText();
  348. },false);
  349. /** To be called immediately before work is sent to the
  350. worker. Updates some UI elements. The 'working'/'end'
  351. event will apply the inverse, undoing the bits this
  352. function does. This impl is not in the 'working'/'start'
  353. event handler because that event is given to us
  354. asynchronously _after_ we need to have performed this
  355. work.
  356. */
  357. const preStartWork = function f(){
  358. if(!f._){
  359. const title = E('title');
  360. f._ = {
  361. btnLabel: btnRender.innerText,
  362. pageTitle: title,
  363. pageTitleOrig: title.innerText
  364. };
  365. }
  366. //f._.pageTitle.innerText = "[working...] "+f._.pageTitleOrig;
  367. btnRender.setAttribute('disabled','disabled');
  368. };
  369. /**
  370. Submits the current input text to pikchr and renders the
  371. result. */
  372. PF.render = function f(txt){
  373. preStartWork();
  374. this.wMsg('pikchr',txt);
  375. };
  376. const eOut = E('#pikchr-output');
  377. const eOutWrapper = E('#pikchr-output-wrapper');
  378. PF.addMsgHandler('pikchr', function(ev){
  379. const m = ev.data;
  380. eOut.classList[m.isError ? 'add' : 'remove']('error');
  381. eOut.dataset.pikchr = m.pikchr;
  382. let content;
  383. let sz;
  384. switch(PF.renderMode){
  385. case 'text':
  386. content = '<textarea>'+m.result+'</textarea>';
  387. eOut.classList.add('text');
  388. eOutWrapper.classList.add('text');
  389. break;
  390. default:
  391. content = m.result;
  392. eOut.classList.remove('text');
  393. eOutWrapper.classList.remove('text');
  394. break;
  395. }
  396. eOut.innerHTML = content;
  397. let vw = null, vh = null;
  398. if(!PF.config.renderAutoScale
  399. && !m.isError && 'html'===PF.renderMode){
  400. const svg = E(eOut,':scope > svg');
  401. const vb = svg ? svg.getAttribute('viewBox').split(' ') : false;
  402. if(vb && 4===vb.length){
  403. vw = (+vb[2] + 10)+'px';
  404. vh = (+vb[3] + 10)+'px';
  405. }else if(svg){
  406. console.warn("SVG element is missing viewBox attribute.");
  407. }
  408. }
  409. eOut.style.width = vw;
  410. eOut.style.height = vh;
  411. })/*'pikchr' msg handler*/;
  412. E('#btn-render-mode').addEventListener('click',function(){
  413. let mode = PF.renderMode;
  414. const modes = ['text','html'];
  415. let ndx = modes.indexOf(mode) + 1;
  416. if(ndx>=modes.length) ndx = 0;
  417. PF.renderMode = modes[ndx];
  418. if(eOut.dataset.pikchr){
  419. PF.render(eOut.dataset.pikchr);
  420. }
  421. });
  422. PF.addMsgHandler('working',function f(ev){
  423. switch(ev.data){
  424. case 'start': /* See notes in preStartWork(). */; return;
  425. case 'end':
  426. //preStartWork._.pageTitle.innerText = preStartWork._.pageTitleOrig;
  427. btnRender.innerText = preStartWork._.btnLabel;
  428. btnRender.removeAttribute('disabled');
  429. return;
  430. }
  431. console.warn("Unhandled 'working' event:",ev.data);
  432. });
  433. /* For each checkbox with data-csstgt, set up a handler which
  434. toggles the given CSS class on the element matching
  435. E(data-csstgt). */
  436. EAll('input[type=checkbox][data-csstgt]')
  437. .forEach(function(e){
  438. const tgt = E(e.dataset.csstgt);
  439. const cssClass = e.dataset.cssclass || 'error';
  440. e.checked = tgt.classList.contains(cssClass);
  441. e.addEventListener('change', function(){
  442. tgt.classList[
  443. this.checked ? 'add' : 'remove'
  444. ](cssClass)
  445. }, false);
  446. });
  447. /* For each checkbox with data-config=X, set up a binding to
  448. PF.config[X]. These must be set up AFTER data-csstgt
  449. checkboxes so that those two states can be synced properly. */
  450. EAll('input[type=checkbox][data-config]')
  451. .forEach(function(e){
  452. const confVal = !!PF.config[e.dataset.config];
  453. if(e.checked !== confVal){
  454. /* Ensure that data-csstgt mappings (if any) get
  455. synced properly. */
  456. e.checked = confVal;
  457. e.dispatchEvent(new Event('change'));
  458. }
  459. e.addEventListener('change', function(){
  460. PF.config[this.dataset.config] = this.checked;
  461. PF.storeConfig();
  462. }, false);
  463. });
  464. E('#opt-cb-autoscale').addEventListener('change',function(){
  465. /* PF.config.renderAutoScale was set by the data-config
  466. event handler. */
  467. if('html'==PF.renderMode && eOut.dataset.pikchr){
  468. PF.render(eOut.dataset.pikchr);
  469. }
  470. });
  471. /* For each button with data-cmd=X, map a click handler which
  472. calls PF.render(X). */
  473. const cmdClick = function(){PF.render(this.dataset.cmd);};
  474. EAll('button[data-cmd]').forEach(
  475. e => e.addEventListener('click', cmdClick, false)
  476. );
  477. /**
  478. TODO: Handle load/import of an external pikchr file.
  479. */
  480. if(0) E('#load-pikchr').addEventListener('change',function(){
  481. const f = this.files[0];
  482. const r = new FileReader();
  483. const status = {loaded: 0, total: 0};
  484. this.setAttribute('disabled','disabled');
  485. const that = this;
  486. r.addEventListener('load', function(){
  487. that.removeAttribute('disabled');
  488. stdout("Loaded",f.name+". Opening pikchr...");
  489. PF.wMsg('open',{
  490. filename: f.name,
  491. buffer: this.result
  492. });
  493. });
  494. r.addEventListener('error',function(){
  495. that.removeAttribute('disabled');
  496. stderr("Loading",f.name,"failed for unknown reasons.");
  497. });
  498. r.addEventListener('abort',function(){
  499. that.removeAttribute('disabled');
  500. stdout("Cancelled loading of",f.name+".");
  501. });
  502. r.readAsArrayBuffer(f);
  503. });
  504. EAll('.fieldset.collapsible').forEach(function(fs){
  505. const legend = E(fs,'span.legend'),
  506. content = EAll(fs,':scope > div');
  507. legend.addEventListener('click', function(){
  508. fs.classList.toggle('collapsed');
  509. content.forEach((d)=>d.classList.toggle('hidden'));
  510. }, false);
  511. });
  512. /**
  513. Given a DOM element, this routine measures its "effective
  514. height", which is the bounding top/bottom range of this element
  515. and all of its children, recursively. For some DOM structure
  516. cases, a parent may have a reported height of 0 even though
  517. children have non-0 sizes.
  518. Returns 0 if !e or if the element really has no height.
  519. */
  520. const effectiveHeight = function f(e){
  521. if(!e) return 0;
  522. if(!f.measure){
  523. f.measure = function callee(e, depth){
  524. if(!e) return;
  525. const m = e.getBoundingClientRect();
  526. if(0===depth){
  527. callee.top = m.top;
  528. callee.bottom = m.bottom;
  529. }else{
  530. callee.top = m.top ? Math.min(callee.top, m.top) : callee.top;
  531. callee.bottom = Math.max(callee.bottom, m.bottom);
  532. }
  533. Array.prototype.forEach.call(e.children,(e)=>callee(e,depth+1));
  534. if(0===depth){
  535. //console.debug("measure() height:",e.className, callee.top, callee.bottom, (callee.bottom - callee.top));
  536. f.extra += callee.bottom - callee.top;
  537. }
  538. return f.extra;
  539. };
  540. }
  541. f.extra = 0;
  542. f.measure(e,0);
  543. return f.extra;
  544. };
  545. btnRender.click();
  546. /**
  547. Returns a function, that, as long as it continues to be invoked,
  548. will not be triggered. The function will be called after it stops
  549. being called for N milliseconds. If `immediate` is passed, call
  550. the callback immediately and hinder future invocations until at
  551. least the given time has passed.
  552. If passed only 1 argument, or passed a falsy 2nd argument,
  553. the default wait time set in this function's $defaultDelay
  554. property is used.
  555. Source: underscore.js, by way of https://davidwalsh.name/javascript-debounce-function
  556. */
  557. const debounce = function f(func, wait, immediate) {
  558. var timeout;
  559. if(!wait) wait = f.$defaultDelay;
  560. return function() {
  561. const context = this, args = Array.prototype.slice.call(arguments);
  562. const later = function() {
  563. timeout = undefined;
  564. if(!immediate) func.apply(context, args);
  565. };
  566. const callNow = immediate && !timeout;
  567. clearTimeout(timeout);
  568. timeout = setTimeout(later, wait);
  569. if(callNow) func.apply(context, args);
  570. };
  571. };
  572. debounce.$defaultDelay = 500 /*arbitrary*/;
  573. /** Debounce handler for auto-rendering while typing. */
  574. const debounceAutoRender = debounce(function f(){
  575. if(!PF._isDirty) return;
  576. const text = getCurrentText();
  577. if(f._ === text){
  578. PF._isDirty = false;
  579. return;
  580. }
  581. f._ = text;
  582. PF._isDirty = false;
  583. PF.render(text || '');
  584. }, 800, false);
  585. taInput.addEventListener('keydown',function f(ev){
  586. if((ev.ctrlKey || ev.shiftKey) && 13 === ev.keyCode){
  587. // Ctrl-enter and shift-enter both run the current input
  588. PF._isDirty = false/*prevent a pending debounce from re-rendering*/;
  589. ev.preventDefault();
  590. ev.stopPropagation();
  591. renderCurrentText();
  592. return;
  593. }
  594. if(!PF.config.renderWhileTyping) return;
  595. /* Auto-render while typing... */
  596. switch(ev.keyCode){
  597. case (ev.keyCode<32): /*any ctrl char*/
  598. /* ^^^ w/o that, simply tapping ctrl is enough to
  599. force a re-render. Similarly, TAB-ing focus away
  600. should not re-render. */
  601. case 33: case 34: /* page up/down */
  602. case 35: case 36: /* home/end */
  603. case 37: case 38: case 39: case 40: /* arrows */
  604. return;
  605. }
  606. PF._isDirty = true;
  607. debounceAutoRender();
  608. }, false);
  609. const ForceResizeKludge = (function(){
  610. /* Workaround for Safari mayhem regarding use of vh CSS
  611. units.... We cannot use vh units to set the main view
  612. size because Safari chokes on that, so we calculate
  613. that height here. Larger than ~95% is too big for
  614. Firefox on Android, causing the input area to move
  615. off-screen. */
  616. const appViews = EAll('.app-view');
  617. const elemsToCount = [
  618. /* Elements which we need to always count in the
  619. visible body size. */
  620. E('body > header'),
  621. E('body > footer')
  622. ];
  623. const resized = function f(){
  624. if(f.$disabled) return;
  625. const wh = window.innerHeight;
  626. var ht;
  627. var extra = 0;
  628. elemsToCount.forEach((e)=>e ? extra += effectiveHeight(e) : false);
  629. ht = wh - extra;
  630. appViews.forEach(function(e){
  631. e.style.height =
  632. e.style.maxHeight = [
  633. "calc(", (ht>=100 ? ht : 100), "px",
  634. " - 2em"/*fudge value*/,")"
  635. /* ^^^^ hypothetically not needed, but both
  636. Chrome/FF on Linux will force scrollbars on the
  637. body if this value is too small. */
  638. ].join('');
  639. });
  640. };
  641. resized.$disabled = true/*gets deleted when setup is finished*/;
  642. window.addEventListener('resize', debounce(resized, 250), false);
  643. return resized;
  644. })()/*ForceResizeKludge*/;
  645. delete ForceResizeKludge.$disabled;
  646. ForceResizeKludge();
  647. }/*onPFLoaded()*/;
  648. })();