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