Fossil SCM

fossil-scm / src / fossil.page.pikchrshowasm.js
Source Blame History 743 lines
7fcb462… stephan 1 /*
7fcb462… stephan 2 2022-05-20
7fcb462… stephan 3
7fcb462… stephan 4 The author disclaims copyright to this source code. In place of a
7fcb462… stephan 5 legal notice, here is a blessing:
7fcb462… stephan 6
7fcb462… stephan 7 * May you do good and not evil.
7fcb462… stephan 8 * May you find forgiveness for yourself and forgive others.
7fcb462… stephan 9 * May you share freely, never taking more than you give.
7fcb462… stephan 10
7fcb462… stephan 11 ***********************************************************************
7fcb462… stephan 12
7fcb462… stephan 13 This is the main entry point for the WASM rendition of fossil's
7fcb462… stephan 14 /pikchrshow app. It sets up the various UI bits, loads a Worker for
7fcb462… stephan 15 the pikchr process, and manages the communication between the UI and
7fcb462… stephan 16 worker.
7fcb462… stephan 17
7fcb462… stephan 18 API dependencies: fossil.dom, fossil.copybutton, fossil.storage
7fcb462… stephan 19 */
7fcb462… stephan 20 (function(F/*fossil object*/){
7fcb462… stephan 21 'use strict';
7fcb462… stephan 22
7fcb462… stephan 23 /* Recall that the 'self' symbol, except where locally
7fcb462… stephan 24 overwritten, refers to the global window or worker object. */
7fcb462… stephan 25
7fcb462… stephan 26 const D = F.dom;
7fcb462… stephan 27 /** Name of the stored copy of this app's config. */
7fcb462… stephan 28 const configStorageKey = 'pikchrshow-config';
7fcb462… stephan 29
7fcb462… stephan 30 /* querySelectorAll() proxy */
7fcb462… stephan 31 const EAll = function(/*[element=document,] cssSelector*/){
7fcb462… stephan 32 return (arguments.length>1 ? arguments[0] : document)
7fcb462… stephan 33 .querySelectorAll(arguments[arguments.length-1]);
7fcb462… stephan 34 };
7fcb462… stephan 35 /* querySelector() proxy */
7fcb462… stephan 36 const E = function(/*[element=document,] cssSelector*/){
7fcb462… stephan 37 return (arguments.length>1 ? arguments[0] : document)
7fcb462… stephan 38 .querySelector(arguments[arguments.length-1]);
7fcb462… stephan 39 };
7fcb462… stephan 40
7fcb462… stephan 41 /** The main application object. */
7fcb462… stephan 42 const PS = {
7fcb462… stephan 43 /* Config options. */
7fcb462… stephan 44 config: {
7fcb462… stephan 45 /* If true, display input/output areas side-by-side, else stack
7fcb462… stephan 46 them vertically. */
7fcb462… stephan 47 sideBySide: true,
7fcb462… stephan 48 /* If true, swap positions of the input/output areas. */
7fcb462… stephan 49 swapInOut: false,
7fcb462… stephan 50 /* If true, the SVG is allowed to resize to fit the parent
7fcb462… stephan 51 content area, else the parent is resized to fit the rendered
7fcb462… stephan 52 SVG (as sized by pikchr). */
7fcb462… stephan 53 renderAutofit: false,
7fcb462… stephan 54 /* If true, automatically render while the user is typing. */
7fcb462… stephan 55 renderWhileTyping: false
7fcb462… stephan 56 },
7fcb462… stephan 57 /* Various DOM elements. */
7fcb462… stephan 58 e: {
7fcb462… stephan 59 previewCopyButton: E('#preview-copy-button'),
7fcb462… stephan 60 previewModeLabel: E('label[for=preview-copy-button]'),
7fcb462… stephan 61 zoneInputButtons: E('.zone-wrapper.input > legend > .button-bar'),
7fcb462… stephan 62 zoneOutputButtons: E('.zone-wrapper.output > legend > .button-bar'),
7fcb462… stephan 63 outText: E('#pikchr-output-text'),
7fcb462… stephan 64 pikOutWrapper: E('#pikchr-output-wrapper'),
7fcb462… stephan 65 pikOut: E('#pikchr-output'),
ff1c48a… stephan 66 btnRender: E('#btn-render')
7fcb462… stephan 67 },
7fcb462… stephan 68 renderModes: ['svg'/*SVG must be at index 0*/,'markdown', 'wiki', 'text'],
7fcb462… stephan 69 renderModeLabels: {
7fcb462… stephan 70 svg: 'SVG', markdown: 'Markdown', wiki: 'Fossil Wiki', text: 'Text'
7fcb462… stephan 71 },
7fcb462… stephan 72 _msgMap: {},
7fcb462… stephan 73 /** Adds a worker message handler for messages of the given
7fcb462… stephan 74 type. */
7fcb462… stephan 75 addMsgHandler: function f(type,callback){
7fcb462… stephan 76 if(Array.isArray(type)){
7fcb462… stephan 77 type.forEach((t)=>this.addMsgHandler(t, callback));
7fcb462… stephan 78 return this;
7fcb462… stephan 79 }
7fcb462… stephan 80 (this._msgMap.hasOwnProperty(type)
7fcb462… stephan 81 ? this._msgMap[type]
7fcb462… stephan 82 : (this._msgMap[type] = [])).push(callback);
7fcb462… stephan 83 return this;
7fcb462… stephan 84 },
7fcb462… stephan 85 /** Given a worker message, runs all handlers for msg.type. */
7fcb462… stephan 86 runMsgHandlers: function(msg){
7fcb462… stephan 87 const list = (this._msgMap.hasOwnProperty(msg.type)
7fcb462… stephan 88 ? this._msgMap[msg.type] : false);
7fcb462… stephan 89 if(!list){
7fcb462… stephan 90 console.warn("No handlers found for message type:",msg);
7fcb462… stephan 91 return false;
7fcb462… stephan 92 }
7fcb462… stephan 93 list.forEach((f)=>f(msg));
7fcb462… stephan 94 return true;
7fcb462… stephan 95 },
7fcb462… stephan 96 /** Removes all message handlers for the given message type. */
7fcb462… stephan 97 clearMsgHandlers: function(type){
7fcb462… stephan 98 delete this._msgMap[type];
7fcb462… stephan 99 return this;
7fcb462… stephan 100 },
7fcb462… stephan 101 /* Posts a message in the form {type, data} to the db worker. Returns this. */
7fcb462… stephan 102 wMsg: function(type,data){
7fcb462… stephan 103 this.worker.postMessage({type, data});
7fcb462… stephan 104 return this;
7fcb462… stephan 105 },
7fcb462… stephan 106 /** Stores this object's config in the browser's storage. */
7fcb462… stephan 107 storeConfig: function(){
7fcb462… stephan 108 F.storage.setJSON(configStorageKey,this.config);
7fcb462… stephan 109 }
7fcb462… stephan 110 };
7fcb462… stephan 111 PS.renderModes.selectedIndex = 0;
7fcb462… stephan 112 PS._config = F.storage.getJSON(configStorageKey);
7fcb462… stephan 113 if(PS._config){
7fcb462… stephan 114 /* Copy all properties to PS.config which are currently in
7fcb462… stephan 115 PS._config. We don't bother copying any other properties: those
7fcb462… stephan 116 would be stale/removed config entries. */
7fcb462… stephan 117 Object.keys(PS.config).forEach(function(k){
7fcb462… stephan 118 if(PS._config.hasOwnProperty(k)){
7fcb462… stephan 119 PS.config[k] = PS._config[k];
7fcb462… stephan 120 }
7fcb462… stephan 121 });
7fcb462… stephan 122 delete PS._config;
7fcb462… stephan 123 }
7fcb462… stephan 124
dd20f34… drh 125 /* Randomize the name of the worker script so that it is never cached.
dd20f34… drh 126 ** The Fossil /builtin method will automatically remove the "-v000000000"
dd20f34… drh 127 ** part of the filename, resolving it to just "pikchr-worker.js". */
dd20f34… drh 128 PS.worker = new Worker('builtin/extsrc/pikchr-worker-v'+
dd20f34… drh 129 (Math.floor(Math.random()*10000000000) + 1000000000)+
dd20f34… drh 130 '.js');
7fcb462… stephan 131 PS.worker.onmessage = (ev)=>PS.runMsgHandlers(ev.data);
7fcb462… stephan 132 PS.addMsgHandler('stdout', console.log.bind(console));
7fcb462… stephan 133 PS.addMsgHandler('stderr', console.error.bind(console));
7fcb462… stephan 134
7fcb462… stephan 135 /** Handles status updates from the Module object. */
7fcb462… stephan 136 PS.addMsgHandler('module', function f(ev){
7fcb462… stephan 137 ev = ev.data;
7fcb462… stephan 138 if('status'!==ev.type){
7fcb462… stephan 139 console.warn("Unexpected module-type message:",ev);
7fcb462… stephan 140 return;
7fcb462… stephan 141 }
7fcb462… stephan 142 if(!f.ui){
7fcb462… stephan 143 f.ui = {
7fcb462… stephan 144 status: E('#module-status'),
7fcb462… stephan 145 progress: E('#module-progress'),
7fcb462… stephan 146 spinner: E('#module-spinner')
7fcb462… stephan 147 };
7fcb462… stephan 148 }
7fcb462… stephan 149 const msg = ev.data;
7fcb462… stephan 150 if(f.ui.progres){
7fcb462… stephan 151 progress.value = msg.step;
7fcb462… stephan 152 progress.max = msg.step + 1/*we don't know how many steps to expect*/;
7fcb462… stephan 153 }
7fcb462… stephan 154 if(1==msg.step){
7fcb462… stephan 155 f.ui.progress.classList.remove('hidden');
7fcb462… stephan 156 f.ui.spinner.classList.remove('hidden');
7fcb462… stephan 157 }
7fcb462… stephan 158 if(msg.text){
7fcb462… stephan 159 f.ui.status.classList.remove('hidden');
7fcb462… stephan 160 f.ui.status.innerText = msg.text;
7fcb462… stephan 161 }else{
7fcb462… stephan 162 if(f.ui.progress){
7fcb462… stephan 163 f.ui.progress.remove();
7fcb462… stephan 164 f.ui.spinner.remove();
7fcb462… stephan 165 delete f.ui.progress;
7fcb462… stephan 166 delete f.ui.spinner;
7fcb462… stephan 167 }
7fcb462… stephan 168 f.ui.status.classList.add('hidden');
7fcb462… stephan 169 /* The module can post messages about fatal problems,
7fcb462… stephan 170 e.g. an exit() being triggered or assertion failure,
7fcb462… stephan 171 after the last "load" message has arrived, so
7fcb462… stephan 172 leave f.ui.status and message listener intact. */
7fcb462… stephan 173 }
7fcb462… stephan 174 });
7fcb462… stephan 175
7fcb462… stephan 176 PS.e.previewModeLabel.innerText =
7fcb462… stephan 177 PS.renderModeLabels[PS.renderModes[PS.renderModes.selectedIndex]];
7fcb462… stephan 178
7fcb462… stephan 179 /**
44cd975… stephan 180 The 'pikchr-ready' event is fired (with no payload) when the
7fcb462… stephan 181 wasm module has finished loading. */
dd20f34… drh 182 PS.addMsgHandler('pikchr-ready', function(event){
44cd975… stephan 183 PS.clearMsgHandlers('pikchr-ready');
dd20f34… drh 184 F.page.onPikchrshowLoaded(event.data);
7fcb462… stephan 185 });
7fcb462… stephan 186
7fcb462… stephan 187 /**
7fcb462… stephan 188 Performs all app initialization which must wait until after the
7fcb462… stephan 189 worker module is loaded. This function removes itself when it's
7fcb462… stephan 190 called.
7fcb462… stephan 191 */
dd20f34… drh 192 F.page.onPikchrshowLoaded = function(pikchrVersion){
7fcb462… stephan 193 delete this.onPikchrshowLoaded;
7fcb462… stephan 194 // Unhide all elements which start out hidden
7fcb462… stephan 195 EAll('.initially-hidden').forEach((e)=>e.classList.remove('initially-hidden'));
7fcb462… stephan 196 const taInput = E('#input');
7fcb462… stephan 197 const btnClearIn = E('#btn-clear');
7fcb462… stephan 198 btnClearIn.addEventListener('click',function(){
7fcb462… stephan 199 taInput.value = '';
7fcb462… stephan 200 },false);
7fcb462… stephan 201 const getCurrentText = function(){
7fcb462… stephan 202 let text;
7fcb462… stephan 203 if(taInput.selectionStart<taInput.selectionEnd){
7fcb462… stephan 204 text = taInput.value.substring(taInput.selectionStart,taInput.selectionEnd).trim();
7fcb462… stephan 205 }else{
7fcb462… stephan 206 text = taInput.value.trim();
7fcb462… stephan 207 }
7fcb462… stephan 208 return text;;
7fcb462… stephan 209 };
7fcb462… stephan 210 const renderCurrentText = function(){
7fcb462… stephan 211 const text = getCurrentText();
7fcb462… stephan 212 if(text) PS.render(text);
7fcb462… stephan 213 };
7fcb462… stephan 214 const setCurrentText = function(txt){
7fcb462… stephan 215 taInput.value = txt;
521da5c… drh 216 renderCurrentText();
7fcb462… stephan 217 };
7fcb462… stephan 218 PS.e.btnRender.addEventListener('click',function(ev){
7fcb462… stephan 219 ev.preventDefault();
7fcb462… stephan 220 renderCurrentText();
7fcb462… stephan 221 },false);
7fcb462… stephan 222
7fcb462… stephan 223 /** To be called immediately before work is sent to the
7fcb462… stephan 224 worker. Updates some UI elements. The 'working'/'end'
7fcb462… stephan 225 event will apply the inverse, undoing the bits this
7fcb462… stephan 226 function does. This impl is not in the 'working'/'start'
7fcb462… stephan 227 event handler because that event is given to us
7fcb462… stephan 228 asynchronously _after_ we need to have performed this
7fcb462… stephan 229 work.
7fcb462… stephan 230 */
7fcb462… stephan 231 const preStartWork = function f(){
7fcb462… stephan 232 if(!f._){
7fcb462… stephan 233 const title = E('title');
7fcb462… stephan 234 f._ = {
7fcb462… stephan 235 pageTitle: title,
7fcb462… stephan 236 pageTitleOrig: title.innerText
7fcb462… stephan 237 };
7fcb462… stephan 238 }
7fcb462… stephan 239 //f._.pageTitle.innerText = "[working...] "+f._.pageTitleOrig;
7fcb462… stephan 240 PS.e.btnRender.setAttribute('disabled','disabled');
7fcb462… stephan 241 };
7fcb462… stephan 242
7fcb462… stephan 243 /**
7fcb462… stephan 244 Submits the current input text to pikchr and renders the
7fcb462… stephan 245 result. */
7fcb462… stephan 246 PS.render = function f(txt){
7fcb462… stephan 247 preStartWork();
7fcb462… stephan 248 this.wMsg('pikchr',{
7fcb462… stephan 249 pikchr: txt,
7fcb462… stephan 250 darkMode: !!window.fossil.config.skin.isDark
7fcb462… stephan 251 });
7fcb462… stephan 252 };
7fcb462… stephan 253
7fcb462… stephan 254 /**
7fcb462… stephan 255 Event handler for 'pikchr' messages from the Worker thread.
7fcb462… stephan 256 */
7fcb462… stephan 257 PS.addMsgHandler('pikchr', function(ev){
7fcb462… stephan 258 const m = ev.data, pikOut = this.e.pikOut;
7fcb462… stephan 259 pikOut.classList[m.isError ? 'add' : 'remove']('error');
7fcb462… stephan 260 pikOut.dataset.pikchr = m.pikchr;
7fcb462… stephan 261 const mode = this.renderModes[this.renderModes.selectedIndex];
7fcb462… stephan 262 switch(mode){
7fcb462… stephan 263 case 'text': case 'markdown': case 'wiki': {
7fcb462… stephan 264 let body;
7fcb462… stephan 265 switch(mode){
7fcb462… stephan 266 case 'markdown':
7fcb462… stephan 267 body = ['```pikchr', m.pikchr, '```'].join('\n');
7fcb462… stephan 268 break;
7fcb462… stephan 269 case 'wiki':
7fcb462… stephan 270 body = ['<verbatim type="pikchr">', m.pikchr, '</verbatim>'].join('');
7fcb462… stephan 271 break;
7fcb462… stephan 272 default:
7fcb462… stephan 273 body = m.result;
7fcb462… stephan 274 }
7fcb462… stephan 275 this.e.outText.value = body;
7fcb462… stephan 276 this.e.outText.classList.remove('hidden');
7fcb462… stephan 277 pikOut.classList.add('hidden');
7fcb462… stephan 278 this.e.pikOutWrapper.classList.add('text');
7fcb462… stephan 279 break;
7fcb462… stephan 280 }
7fcb462… stephan 281 case 'svg':
7fcb462… stephan 282 this.e.outText.classList.add('hidden');
7fcb462… stephan 283 pikOut.classList.remove('hidden');
7fcb462… stephan 284 this.e.pikOutWrapper.classList.remove('text');
7fcb462… stephan 285 pikOut.innerHTML = m.result;
7fcb462… stephan 286 this.e.outText.value = m.result/*for clipboard copy*/;
7fcb462… stephan 287 break;
7fcb462… stephan 288 default: throw new Error("Unhandled render mode: "+mode);
7fcb462… stephan 289 }
7fcb462… stephan 290 let vw = null, vh = null;
7fcb462… stephan 291 if('svg'===mode){
7fcb462… stephan 292 if(m.isError){
7fcb462… stephan 293 vw = vh = '100%';
7fcb462… stephan 294 }else if(this.config.renderAutofit){
7fcb462… stephan 295 /* FIXME: current behavior doesn't work as desired when width>height
7fcb462… stephan 296 (e.g. non-side-by-side mode).*/
7fcb462… stephan 297 vw = vh = '98%';
7fcb462… stephan 298 }else{
7fcb462… stephan 299 vw = m.width+1+'px'; vh = m.height+1+'px';
7fcb462… stephan 300 /* +1 is b/c the SVG uses floating point sizes but pikchr()
7fcb462… stephan 301 returns truncated integers. */
7fcb462… stephan 302 }
7fcb462… stephan 303 pikOut.style.width = vw;
7fcb462… stephan 304 pikOut.style.height = vh;
7fcb462… stephan 305 }
7fcb462… stephan 306 }.bind(PS))/*'pikchr' msg handler*/;
7fcb462… stephan 307
7fcb462… stephan 308 E('#btn-render-mode').addEventListener('click',function(){
7fcb462… stephan 309 const modes = this.renderModes;
7fcb462… stephan 310 modes.selectedIndex = (modes.selectedIndex + 1) % modes.length;
7fcb462… stephan 311 this.e.previewModeLabel.innerText = this.renderModeLabels[modes[modes.selectedIndex]];
7fcb462… stephan 312 if(this.e.pikOut.dataset.pikchr){
7fcb462… stephan 313 this.render(this.e.pikOut.dataset.pikchr);
7fcb462… stephan 314 }
7fcb462… stephan 315 }.bind(PS));
7fcb462… stephan 316 F.copyButton(PS.e.previewCopyButton, {copyFromElement: PS.e.outText});
7fcb462… stephan 317
7fcb462… stephan 318 PS.addMsgHandler('working',function f(ev){
7fcb462… stephan 319 switch(ev.data){
7fcb462… stephan 320 case 'start': /* See notes in preStartWork(). */; return;
7fcb462… stephan 321 case 'end':
7fcb462… stephan 322 //preStartWork._.pageTitle.innerText = preStartWork._.pageTitleOrig;
7fcb462… stephan 323 this.e.btnRender.removeAttribute('disabled');
7fcb462… stephan 324 this.e.pikOutWrapper.classList[this.config.renderAutofit ? 'add' : 'remove']('autofit');
7fcb462… stephan 325 return;
7fcb462… stephan 326 }
7fcb462… stephan 327 console.warn("Unhandled 'working' event:",ev.data);
7fcb462… stephan 328 }.bind(PS));
7fcb462… stephan 329
7fcb462… stephan 330 /* For each checkbox with data-csstgt, set up a handler which
7fcb462… stephan 331 toggles the given CSS class on the element matching
7fcb462… stephan 332 E(data-csstgt). */
7fcb462… stephan 333 EAll('input[type=checkbox][data-csstgt]')
7fcb462… stephan 334 .forEach(function(e){
7fcb462… stephan 335 const tgt = E(e.dataset.csstgt);
7fcb462… stephan 336 const cssClass = e.dataset.cssclass || 'error';
7fcb462… stephan 337 e.checked = tgt.classList.contains(cssClass);
7fcb462… stephan 338 e.addEventListener('change', function(){
7fcb462… stephan 339 tgt.classList[
7fcb462… stephan 340 this.checked ? 'add' : 'remove'
7fcb462… stephan 341 ](cssClass)
7fcb462… stephan 342 }, false);
7fcb462… stephan 343 });
7fcb462… stephan 344 /* For each checkbox with data-config=X, set up a binding to
7fcb462… stephan 345 PS.config[X]. These must be set up AFTER data-csstgt
7fcb462… stephan 346 checkboxes so that those two states can be synced properly. */
7fcb462… stephan 347 EAll('input[type=checkbox][data-config]')
7fcb462… stephan 348 .forEach(function(e){
7fcb462… stephan 349 const confVal = !!PS.config[e.dataset.config];
7fcb462… stephan 350 if(e.checked !== confVal){
7fcb462… stephan 351 /* Ensure that data-csstgt mappings (if any) get
7fcb462… stephan 352 synced properly. */
7fcb462… stephan 353 e.checked = confVal;
7fcb462… stephan 354 e.dispatchEvent(new Event('change'));
7fcb462… stephan 355 }
7fcb462… stephan 356 e.addEventListener('change', function(){
7fcb462… stephan 357 PS.config[this.dataset.config] = this.checked;
7fcb462… stephan 358 PS.storeConfig();
7fcb462… stephan 359 }, false);
7fcb462… stephan 360 });
7fcb462… stephan 361 E('#opt-cb-autofit').addEventListener('change',function(){
7fcb462… stephan 362 /* PS.config.renderAutofit was set by the data-config
7fcb462… stephan 363 event handler. */
7fcb462… stephan 364 if(0==PS.renderModes.selectedIndex && PS.e.pikOut.dataset.pikchr){
7fcb462… stephan 365 PS.render(PS.e.pikOut.dataset.pikchr);
7fcb462… stephan 366 }
7fcb462… stephan 367 });
7fcb462… stephan 368 /* For each button with data-cmd=X, map a click handler which
7fcb462… stephan 369 calls PS.render(X). */
7fcb462… stephan 370 const cmdClick = function(){PS.render(this.dataset.cmd);};
7fcb462… stephan 371 EAll('button[data-cmd]').forEach(
7fcb462… stephan 372 e => e.addEventListener('click', cmdClick, false)
7fcb462… stephan 373 );
7fcb462… stephan 374
7fcb462… stephan 375
7fcb462… stephan 376 ////////////////////////////////////////////////////////////
7fcb462… stephan 377 // Set up selection list of predefined scripts...
7fcb462… stephan 378 if(true){
7fcb462… stephan 379 const selectScript = PS.e.selectScript = D.select();
7fcb462… stephan 380 D.append(PS.e.zoneInputButtons, selectScript);
7fcb462… stephan 381 PS.predefinedPiks.forEach(function(script,ndx){
7fcb462… stephan 382 const opt = D.option(script.code ? script.code.trim() :'', script.name);
7fcb462… stephan 383 D.append(selectScript, opt);
7fcb462… stephan 384 if(!ndx) selectScript.selectedIndex = 0 /*timing/ordering workaround*/;
7fcb462… stephan 385 if(ndx && !script.code){
7fcb462… stephan 386 /* Treat entries w/ no code as separators EXCEPT for the
7fcb462… stephan 387 first one, which we want to keep selectable solely for
7fcb462… stephan 388 cosmetic reasons. */
7fcb462… stephan 389 D.disable(opt);
7fcb462… stephan 390 }
7fcb462… stephan 391 });
7fcb462… stephan 392 delete PS.predefinedPiks;
7fcb462… stephan 393 selectScript.addEventListener('change', function(ev){
7fcb462… stephan 394 const val = ev.target.value;
7fcb462… stephan 395 if(!val) return;
7fcb462… stephan 396 setCurrentText(val);
7fcb462… stephan 397 }, false);
7fcb462… stephan 398 }/*Examples*/
7fcb462… stephan 399
7fcb462… stephan 400 /**
7b4bbc8… stephan 401 TODO? Handle load/import of an external pikchr file.
7fcb462… stephan 402 */
7fcb462… stephan 403 if(0) E('#load-pikchr').addEventListener('change',function(){
7fcb462… stephan 404 const f = this.files[0];
7fcb462… stephan 405 const r = new FileReader();
7fcb462… stephan 406 const status = {loaded: 0, total: 0};
7fcb462… stephan 407 this.setAttribute('disabled','disabled');
7fcb462… stephan 408 const that = this;
7fcb462… stephan 409 r.addEventListener('load', function(){
7fcb462… stephan 410 that.removeAttribute('disabled');
7fcb462… stephan 411 stdout("Loaded",f.name+". Opening pikchr...");
7fcb462… stephan 412 PS.wMsg('open',{
7fcb462… stephan 413 filename: f.name,
7fcb462… stephan 414 buffer: this.result
7fcb462… stephan 415 });
7fcb462… stephan 416 });
7fcb462… stephan 417 r.addEventListener('error',function(){
7fcb462… stephan 418 that.removeAttribute('disabled');
7fcb462… stephan 419 stderr("Loading",f.name,"failed for unknown reasons.");
7fcb462… stephan 420 });
7fcb462… stephan 421 r.addEventListener('abort',function(){
7fcb462… stephan 422 that.removeAttribute('disabled');
7fcb462… stephan 423 stdout("Cancelled loading of",f.name+".");
7fcb462… stephan 424 });
7fcb462… stephan 425 r.readAsArrayBuffer(f);
7fcb462… stephan 426 });
7fcb462… stephan 427
7fcb462… stephan 428 EAll('fieldset.collapsible').forEach(function(fs){
864ed8d… stephan 429 const btnToggle = E(fs,'legend > .fieldset-toggle'),
7fcb462… stephan 430 content = EAll(fs,':scope > div');
7fcb462… stephan 431 btnToggle.addEventListener('click', function(){
7fcb462… stephan 432 fs.classList.toggle('collapsed');
7fcb462… stephan 433 content.forEach((d)=>d.classList.toggle('hidden'));
7fcb462… stephan 434 }, false);
7fcb462… stephan 435 });
7fcb462… stephan 436
521da5c… drh 437 if(window.sessionStorage){
521da5c… drh 438 /* If sessionStorage['pikchr-xfer'] exists and the "fromSession"
521da5c… drh 439 URL argument was passed to this page, load the pikchr source
521da5c… drh 440 from the session. This is used by the "open in pikchrshow"
521da5c… drh 441 link in the forum. */
521da5c… drh 442 const src = window.sessionStorage.getItem('pikchr-xfer');
521da5c… drh 443 if( src && (new URL(self.location.href).searchParams).has('fromSession') ){
521da5c… drh 444 taInput.value = src;
521da5c… drh 445 window.sessionStorage.removeItem('pikchr-xfer');
521da5c… drh 446 }
521da5c… drh 447 }
dd20f34… drh 448 D.append(E('fieldset.options > div'),
dd20f34… drh 449 D.append(D.addClass(D.span(), 'labeled-input'),
dd20f34… drh 450 'pikchr v. '+pikchrVersion));
521da5c… drh 451
7fcb462… stephan 452 PS.e.btnRender.click();
521da5c… drh 453
7fcb462… stephan 454 /** Debounce handler for auto-rendering while typing. */
7fcb462… stephan 455 const debounceAutoRender = F.debounce(function f(){
7fcb462… stephan 456 if(!PS._isDirty) return;
7fcb462… stephan 457 const text = getCurrentText();
7fcb462… stephan 458 if(f._ === text){
7fcb462… stephan 459 PS._isDirty = false;
7fcb462… stephan 460 return;
7fcb462… stephan 461 }
7fcb462… stephan 462 f._ = text;
7fcb462… stephan 463 PS._isDirty = false;
7fcb462… stephan 464 PS.render(text || '');
7fcb462… stephan 465 }, 800, false);
7fcb462… stephan 466
7fcb462… stephan 467 taInput.addEventListener('keydown',function f(ev){
7fcb462… stephan 468 if((ev.ctrlKey || ev.shiftKey) && 13 === ev.keyCode){
7fcb462… stephan 469 // Ctrl-enter and shift-enter both run the current input
7fcb462… stephan 470 PS._isDirty = false/*prevent a pending debounce from re-rendering*/;
7fcb462… stephan 471 ev.preventDefault();
7fcb462… stephan 472 ev.stopPropagation();
7fcb462… stephan 473 renderCurrentText();
7fcb462… stephan 474 return;
7fcb462… stephan 475 }
7fcb462… stephan 476 if(!PS.config.renderWhileTyping) return;
7fcb462… stephan 477 /* Auto-render while typing... */
7fcb462… stephan 478 switch(ev.keyCode){
7fcb462… stephan 479 case (ev.keyCode<32): /*any ctrl char*/
7fcb462… stephan 480 /* ^^^ w/o that, simply tapping ctrl is enough to
7fcb462… stephan 481 force a re-render. Similarly, TAB-ing focus away
7fcb462… stephan 482 should not re-render. */
7fcb462… stephan 483 case 33: case 34: /* page up/down */
7fcb462… stephan 484 case 35: case 36: /* home/end */
7fcb462… stephan 485 case 37: case 38: case 39: case 40: /* arrows */
7fcb462… stephan 486 return;
7fcb462… stephan 487 }
7fcb462… stephan 488 PS._isDirty = true;
7fcb462… stephan 489 debounceAutoRender();
7fcb462… stephan 490 }, false);
7fcb462… stephan 491
7fcb462… stephan 492 const ForceResizeKludge = (function(){
7fcb462… stephan 493 /* Workaround for Safari mayhem regarding use of vh CSS
7fcb462… stephan 494 units.... We cannot use vh units to set the main view
7fcb462… stephan 495 size because Safari chokes on that, so we calculate
7fcb462… stephan 496 that height here. Larger than ~95% is too big for
7fcb462… stephan 497 Firefox on Android, causing the input area to move
7fcb462… stephan 498 off-screen. */
7fcb462… stephan 499 const appViews = EAll('.app-view');
7fcb462… stephan 500 const elemsToCount = [
7fcb462… stephan 501 /* Elements which we need to always count in the
7fcb462… stephan 502 visible body size. */
7b4bbc8… stephan 503 E('body > header'),
7b4bbc8… stephan 504 E('body > nav.mainmenu'),
7b4bbc8… stephan 505 E('body > footer')
7fcb462… stephan 506 ];
7fcb462… stephan 507 const resized = function f(){
7fcb462… stephan 508 if(f.$disabled) return;
7fcb462… stephan 509 const wh = window.innerHeight;
7fcb462… stephan 510 var ht;
7fcb462… stephan 511 var extra = 0;
7fcb462… stephan 512 elemsToCount.forEach((e)=>e ? extra += F.dom.effectiveHeight(e) : false);
7fcb462… stephan 513 ht = wh - extra;
7fcb462… stephan 514 appViews.forEach(function(e){
7fcb462… stephan 515 e.style.height =
7fcb462… stephan 516 e.style.maxHeight = [
7fcb462… stephan 517 "calc(", (ht>=100 ? ht : 100), "px",
7fcb462… stephan 518 " - 2em"/*fudge value*/,")"
7fcb462… stephan 519 /* ^^^^ hypothetically not needed, but both
7fcb462… stephan 520 Chrome/FF on Linux will force scrollbars on the
7fcb462… stephan 521 body if this value is too small. */
7fcb462… stephan 522 ].join('');
7fcb462… stephan 523 });
7fcb462… stephan 524 };
7fcb462… stephan 525 resized.$disabled = true/*gets deleted when setup is finished*/;
7fcb462… stephan 526 window.addEventListener('resize', F.debounce(resized, 250), false);
7fcb462… stephan 527 return resized;
7fcb462… stephan 528 })()/*ForceResizeKludge*/;
7fcb462… stephan 529
7fcb462… stephan 530 delete ForceResizeKludge.$disabled;
7fcb462… stephan 531 ForceResizeKludge();
7fcb462… stephan 532 }/*onPikchrshowLoaded()*/;
7fcb462… stephan 533
7fcb462… stephan 534
7fcb462… stephan 535 /**
44e1c41… stephan 536 Predefined example pikchr scripts. Each entry is an object:
7fcb462… stephan 537
7fcb462… stephan 538 {
7fcb462… stephan 539 name: required string,
7fcb462… stephan 540 code: optional code string. An entry with a falsy code is treated
7fcb462… stephan 541 like a separator in the resulting SELECT element (a
7fcb462… stephan 542 disabled OPTION).
7fcb462… stephan 543 }
7fcb462… stephan 544 */
7fcb462… stephan 545 PS.predefinedPiks = [
7fcb462… stephan 546 {name: "-- Example Scripts --", code: false},
7fcb462… stephan 547 /*
7fcb462… stephan 548 The following were imported from the pikchr test scripts:
7fcb462… stephan 549
7fcb462… stephan 550 https://fossil-scm.org/pikchr/dir/examples
7fcb462… stephan 551 */
7fcb462… stephan 552 {name:"Cardinal headings",code:` linerad = 5px
7fcb462… stephan 553 C: circle "Center" rad 150%
7fcb462… stephan 554 circle "N" at 1.0 n of C; arrow from C to last chop ->
7fcb462… stephan 555 circle "NE" at 1.0 ne of C; arrow from C to last chop <-
7fcb462… stephan 556 circle "E" at 1.0 e of C; arrow from C to last chop <->
7fcb462… stephan 557 circle "SE" at 1.0 se of C; arrow from C to last chop ->
7fcb462… stephan 558 circle "S" at 1.0 s of C; arrow from C to last chop <-
7fcb462… stephan 559 circle "SW" at 1.0 sw of C; arrow from C to last chop <->
7fcb462… stephan 560 circle "W" at 1.0 w of C; arrow from C to last chop ->
7fcb462… stephan 561 circle "NW" at 1.0 nw of C; arrow from C to last chop <-
7fcb462… stephan 562 arrow from 2nd circle to 3rd circle chop
7fcb462… stephan 563 arrow from 4th circle to 3rd circle chop
7fcb462… stephan 564 arrow from SW to S chop <->
7fcb462… stephan 565 circle "ESE" at 2.0 heading 112.5 from Center \
7fcb462… stephan 566 thickness 150% fill lightblue radius 75%
7fcb462… stephan 567 arrow from Center to ESE thickness 150% <-> chop
7fcb462… stephan 568 arrow from ESE up 1.35 then to NE chop
7fcb462… stephan 569 line dashed <- from E.e to (ESE.x,E.y)
7fcb462… stephan 570 line dotted <-> thickness 50% from N to NW chop
7fcb462… stephan 571 `},{name:"Core object types",code:`AllObjects: [
7fcb462… stephan 572
7fcb462… stephan 573 # First row of objects
7fcb462… stephan 574 box "box"
7fcb462… stephan 575 box rad 10px "box (with" "rounded" "corners)" at 1in right of previous
7fcb462… stephan 576 circle "circle" at 1in right of previous
7fcb462… stephan 577 ellipse "ellipse" at 1in right of previous
7fcb462… stephan 578
7fcb462… stephan 579 # second row of objects
7fcb462… stephan 580 OVAL1: oval "oval" at 1in below first box
7fcb462… stephan 581 oval "(tall &" "thin)" "oval" width OVAL1.height height OVAL1.width \
7fcb462… stephan 582 at 1in right of previous
7fcb462… stephan 583 cylinder "cylinder" at 1in right of previous
7fcb462… stephan 584 file "file" at 1in right of previous
7fcb462… stephan 585
7fcb462… stephan 586 # third row shows line-type objects
7fcb462… stephan 587 dot "dot" above at 1in below first oval
7fcb462… stephan 588 line right from 1.8cm right of previous "lines" above
7fcb462… stephan 589 arrow right from 1.8cm right of previous "arrows" above
7fcb462… stephan 590 spline from 1.8cm right of previous \
7fcb462… stephan 591 go right .15 then .3 heading 30 then .5 heading 160 then .4 heading 20 \
7fcb462… stephan 592 then right .15
7fcb462… stephan 593 "splines" at 3rd vertex of previous
7fcb462… stephan 594
7fcb462… stephan 595 # The third vertex of the spline is not actually on the drawn
7fcb462… stephan 596 # curve. The third vertex is a control point. To see its actual
7fcb462… stephan 597 # position, uncomment the following line:
7fcb462… stephan 598 #dot color red at 3rd vertex of previous spline
7fcb462… stephan 599
7fcb462… stephan 600 # Draw various lines below the first line
7fcb462… stephan 601 line dashed right from 0.3cm below start of previous line
7fcb462… stephan 602 line dotted right from 0.3cm below start of previous
7fcb462… stephan 603 line thin right from 0.3cm below start of previous
7fcb462… stephan 604 line thick right from 0.3cm below start of previous
7fcb462… stephan 605
7fcb462… stephan 606
7fcb462… stephan 607 # Draw arrows with different arrowhead configurations below
7fcb462… stephan 608 # the first arrow
7fcb462… stephan 609 arrow <- right from 0.4cm below start of previous arrow
7fcb462… stephan 610 arrow <-> right from 0.4cm below start of previous
7fcb462… stephan 611
7fcb462… stephan 612 # Draw splines with different arrowhead configurations below
7fcb462… stephan 613 # the first spline
7fcb462… stephan 614 spline same from .4cm below start of first spline ->
7fcb462… stephan 615 spline same from .4cm below start of previous <-
7fcb462… stephan 616 spline same from .4cm below start of previous <->
7fcb462… stephan 617
7fcb462… stephan 618 ] # end of AllObjects
7fcb462… stephan 619
7fcb462… stephan 620 # Label the whole diagram
7fcb462… stephan 621 text "Examples Of Pikchr Objects" big bold at .8cm above north of AllObjects
7fcb462… stephan 622 `},{name:"Swimlanes",code:` $laneh = 0.75
7fcb462… stephan 623
7fcb462… stephan 624 # Draw the lanes
7fcb462… stephan 625 down
7fcb462… stephan 626 box width 3.5in height $laneh fill 0xacc9e3
7fcb462… stephan 627 box same fill 0xc5d8ef
7fcb462… stephan 628 box same as first box
7fcb462… stephan 629 box same as 2nd box
7fcb462… stephan 630 line from 1st box.sw+(0.2,0) up until even with 1st box.n \
7fcb462… stephan 631 "Alan" above aligned
7fcb462… stephan 632 line from 2nd box.sw+(0.2,0) up until even with 2nd box.n \
7fcb462… stephan 633 "Betty" above aligned
7fcb462… stephan 634 line from 3rd box.sw+(0.2,0) up until even with 3rd box.n \
7fcb462… stephan 635 "Charlie" above aligned
7fcb462… stephan 636 line from 4th box.sw+(0.2,0) up until even with 4th box.n \
7fcb462… stephan 637 "Darlene" above aligned
7fcb462… stephan 638
7fcb462… stephan 639 # fill in content for the Alice lane
7fcb462… stephan 640 right
7fcb462… stephan 641 A1: circle rad 0.1in at end of first line + (0.2,-0.2) \
521da5c… drh 642 fill white thickness 1.5px "1"
7fcb462… stephan 643 arrow right 50%
7fcb462… stephan 644 circle same "2"
7fcb462… stephan 645 arrow right until even with first box.e - (0.65,0.0)
7fcb462… stephan 646 ellipse "future" fit fill white height 0.2 width 0.5 thickness 1.5px
7fcb462… stephan 647 A3: circle same at A1+(0.8,-0.3) "3" fill 0xc0c0c0
7fcb462… stephan 648 arrow from A1 to last circle chop "fork!" below aligned
7fcb462… stephan 649
7fcb462… stephan 650 # content for the Betty lane
7fcb462… stephan 651 B1: circle same as A1 at A1-(0,$laneh) "1"
7fcb462… stephan 652 arrow right 50%
7fcb462… stephan 653 circle same "2"
7fcb462… stephan 654 arrow right until even with first ellipse.w
7fcb462… stephan 655 ellipse same "future"
7fcb462… stephan 656 B3: circle same at A3-(0,$laneh) "3"
7fcb462… stephan 657 arrow right 50%
7fcb462… stephan 658 circle same as A3 "4"
7fcb462… stephan 659 arrow from B1 to 2nd last circle chop
7fcb462… stephan 660
7fcb462… stephan 661 # content for the Charlie lane
7fcb462… stephan 662 C1: circle same as A1 at B1-(0,$laneh) "1"
7fcb462… stephan 663 arrow 50%
7fcb462… stephan 664 circle same "2"
7fcb462… stephan 665 arrow right 0.8in "goes" "offline"
7fcb462… stephan 666 C5: circle same as A3 "5"
7fcb462… stephan 667 arrow right until even with first ellipse.w \
7fcb462… stephan 668 "back online" above "pushes 5" below "pulls 3 & 4" below
7fcb462… stephan 669 ellipse same "future"
7fcb462… stephan 670
7fcb462… stephan 671 # content for the Darlene lane
7fcb462… stephan 672 D1: circle same as A1 at C1-(0,$laneh) "1"
7fcb462… stephan 673 arrow 50%
7fcb462… stephan 674 circle same "2"
7fcb462… stephan 675 arrow right until even with C5.w
7fcb462… stephan 676 circle same "5"
7fcb462… stephan 677 arrow 50%
7fcb462… stephan 678 circle same as A3 "6"
7fcb462… stephan 679 arrow right until even with first ellipse.w
7fcb462… stephan 680 ellipse same "future"
7fcb462… stephan 681 D3: circle same as B3 at B3-(0,2*$laneh) "3"
7fcb462… stephan 682 arrow 50%
7fcb462… stephan 683 circle same "4"
7fcb462… stephan 684 arrow from D1 to D3 chop
7fcb462… stephan 685 `},{
7fcb462… stephan 686 name: "The Stuff of Dreams",
7fcb462… stephan 687 code:`
7fcb462… stephan 688 O: text "DREAMS" color grey
7fcb462… stephan 689 circle rad 0.9 at 0.6 above O thick color red
7fcb462… stephan 690 text "INEXPENSIVE" big bold at 0.9 above O color red
7fcb462… stephan 691
7fcb462… stephan 692 circle rad 0.9 at 0.6 heading 120 from O thick color green
7fcb462… stephan 693 text "FAST" big bold at 0.9 heading 120 from O color green
7fcb462… stephan 694
7fcb462… stephan 695 circle rad 0.9 at 0.6 heading -120 from O thick color blue
7fcb462… stephan 696 text "HIGH" big bold "QUALITY" big bold at 0.9 heading -120 from O color blue
7fcb462… stephan 697
7fcb462… stephan 698 text "EXPENSIVE" at 0.55 below O color cyan
7fcb462… stephan 699 text "SLOW" at 0.55 heading -60 from O color magenta
7fcb462… stephan 700 text "POOR" "QUALITY" at 0.55 heading 60 from O color gold
ff1c48a… stephan 701 `},{name:"Precision Arrows",code:`
ff1c48a… stephan 702 # Source: https://pikchr.org/home/forumpost/7f2f9a03eb
ff1c48a… stephan 703 define quiver {
ff1c48a… stephan 704 dot invis at 0.5 < $1.ne , $1.e >
ff1c48a… stephan 705 dot invis at 0.5 < $1.nw , $1.w >
ff1c48a… stephan 706 dot invis at 0.5 < $1.se , $1.e >
ff1c48a… stephan 707 dot invis at 0.5 < $1.sw , $1.w >
ff1c48a… stephan 708
ff1c48a… stephan 709 dot at $2 right of 4th previous dot
ff1c48a… stephan 710 dot at $3 right of 4th previous dot
ff1c48a… stephan 711 dot at $4 right of 4th previous dot
ff1c48a… stephan 712 dot at $5 right of 4th previous dot
ff1c48a… stephan 713 arrow <- from previous dot to 2nd previous dot
ff1c48a… stephan 714 arrow -> from 3rd previous dot to 4th previous dot
ff1c48a… stephan 715 }
ff1c48a… stephan 716
ff1c48a… stephan 717 define show_compass_l {
ff1c48a… stephan 718 dot color red at $1.e " .e" ljust
ff1c48a… stephan 719 dot same at $1.ne " .ne" ljust above
ff1c48a… stephan 720 line thick color green from previous to 2nd last dot
ff1c48a… stephan 721 }
ff1c48a… stephan 722
ff1c48a… stephan 723 define show_compass_r {
ff1c48a… stephan 724 dot color red at $1.w " .w" ljust
ff1c48a… stephan 725 dot same at $1.nw " .nw" ljust above
ff1c48a… stephan 726 line thick color green from previous to 2nd last dot
ff1c48a… stephan 727 }
ff1c48a… stephan 728
ff1c48a… stephan 729 PROGRAM: file "Program" rad 45px
ff1c48a… stephan 730 show_compass_l(PROGRAM)
ff1c48a… stephan 731 QUIVER: box invis ht 0.75
ff1c48a… stephan 732 DATABASE: oval "Database" ht 0.75 wid 1.1
ff1c48a… stephan 733 show_compass_r(DATABASE)
ff1c48a… stephan 734
ff1c48a… stephan 735 quiver(QUIVER, 5px, -5px, 5px, 0px)
ff1c48a… stephan 736
ff1c48a… stephan 737 text "Query" with .c at 0.1in above last arrow
ff1c48a… stephan 738 text "Records" with .c at 0.1in below 2nd last arrow
7fcb462… stephan 739 `}
7fcb462… stephan 740 ];
7fcb462… stephan 741
7fcb462… stephan 742
7fcb462… stephan 743 })(window.fossil);

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button