1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
| (function(F/*the fossil object*/){
"use strict";
/**
Client-side implementation of the /pikchrshow app. Requires that
the fossil JS bootstrapping is complete and that these fossil JS
APIs have been installed: fossil.fetch, fossil.dom,
fossil.copybutton, fossil.popupwidget
*/
const E = (s)=>document.querySelector(s),
D = F.dom,
P = F.page;
P.previewMode = 0 /*0==rendered SVG, 1==pikchr text markdown,
2==pikchr text fossil, 3==raw SVG. */
P.response = {/*stashed state for the server's preview response*/
isError: false,
inputText: undefined /* value of the editor field at render-time */,
raw: undefined /* raw response text/HTML from server */
};
F.onPageLoad(function() {
document.body.classList.add('pikchrshow');
P.e = { /* various DOM elements we work with... */
previewTarget: E('#pikchrshow-output'),
previewLegend: E('#pikchrshow-output-wrapper > legend'),
previewCopyButton: D.attr(
D.addClass(D.span(),'copy-button'),
'id','preview-copy-button'
),
previewModeLabel: D.label('preview-copy-button'),
btnSubmit: E('#pikchr-submit-preview'),
cbDarkMode: E('#flipcolors-wrapper > input[type=checkbox]'),
taContent: E('#content'),
taPreviewText: D.textarea(20,0,true),
uiControls: E('#pikchrshow-controls'),
previewModeToggle: D.button("Preview mode"),
markupAlignDefault: D.attr(D.radio('markup-align','',true),
'id','markup-align-default'),
markupAlignCenter: D.attr(D.radio('markup-align','center'),
'id','markup-align-center'),
markupAlignIndent: D.attr(D.radio('markup-align','indent'),
'id','markup-align-indent'),
markupAlignWrapper: D.addClass(D.span(), 'input-with-label')
};
////////////////////////////////////////////////////////////
// Setup markup alignment selection...
const alignEvent = function(ev){
/* Update markdown/fossil wiki preview if it's active */
if(P.previewMode==1 || P.previewMode==2){
P.renderPreview();
}
};
P.e.markupAlignRadios = [
P.e.markupAlignDefault,
P.e.markupAlignCenter,
P.e.markupAlignIndent
];
D.append(P.e.markupAlignWrapper,
D.addClass(D.append(D.span(),"align:"),
'v-align-middle'));
P.e.markupAlignRadios.forEach(
function(e){
e.addEventListener('change', alignEvent, false);
D.append(P.e.markupAlignWrapper,
D.addClass([
e,
D.label(e, e.value || "left")
], 'v-align-middle'));
}
);
////////////////////////////////////////////////////////////
// Setup the preview fieldset's LEGEND element...
D.append( P.e.previewLegend,
P.e.previewModeToggle,
'\u00a0',
P.e.previewCopyButton,
P.e.previewModeLabel,
P.e.markupAlignWrapper );
////////////////////////////////////////////////////////////
// Setup clipboard-copy of markup/SVG...
F.copyButton(P.e.previewCopyButton, {copyFromElement: P.e.taPreviewText});
P.e.previewCopyButton.addEventListener('text-copied', D.flashOnce.eventHandler, false);
P.e.previewModeLabel.addEventListener('click', ()=>P.e.previewCopyButton.click(), false);
////////////////////////////////////////////////////////////
// Set up dark mode simulator...
P.e.cbDarkMode.addEventListener('change', function(ev){
if(ev.target.checked) D.addClass(P.e.previewTarget, 'dark-mode');
else D.removeClass(P.e.previewTarget, 'dark-mode');
}, false);
if(P.e.cbDarkMode.checked) D.addClass(P.e.previewTarget, 'dark-mode');
////////////////////////////////////////////////////////////
// Set up preview update and preview mode toggle...
P.e.btnSubmit.addEventListener('click', ()=>P.preview(), false);
P.e.previewModeToggle.addEventListener('click', function(){
/* Rotate through the 4 available preview modes */
P.previewMode = ++P.previewMode % 4;
P.renderPreview();
}, false);
////////////////////////////////////////////////////////////
// Set up selection list of predefined scripts...
if(true){
const selectScript = P.e.selectScript = D.select(),
cbAutoPreview = P.e.cbAutoPreview =
D.attr(D.checkbox(true),'id', 'cb-auto-preview'),
cbWrap = D.addClass(D.div(),'input-with-label')
;
D.append(
cbWrap,
selectScript,
cbAutoPreview,
D.label(cbAutoPreview,"Auto-preview?"),
F.helpButtonlets.create(
D.append(D.div(),
'Auto-preview automatically previews selected ',
'built-in pikchr scripts by sending them to ',
'the server for rendering. Not recommended on a ',
'slow connection/server.',
D.br(),D.br(),
'Pikchr scripts may also be dragged/dropped from ',
'the local filesystem into the text area, if the ',
'environment supports it, but the auto-preview ',
'option does not apply to them.'
)
)
)/*.childNodes.forEach(function(ch){
ch.style.margin = "0 0.25em";
})*/;
D.append(P.e.uiControls, cbWrap);
P.predefinedPiks.forEach(function(script,ndx){
const opt = D.option(script.code ? script.code.trim() :'', script.name);
D.append(selectScript, opt);
opt.$_sampleScript = script /* for response caching purposes */;
if(!ndx) selectScript.selectedIndex = 0 /*timing/ordering workaround*/;
if(!script.code) D.disable(opt);
});
delete P.predefinedPiks;
selectScript.addEventListener('change', function(ev){
const val = ev.target.value;
if(!val) return;
const opt = ev.target.selectedOptions[0];
P.e.taContent.value = val;
if(cbAutoPreview.checked){
P.preview.$_sampleScript = opt.$_sampleScript;
P.preview();
}
}, false);
}
////////////////////////////////////////////////////////////
// Move dark mode checkbox to the end and add a help buttonlet
D.append(
P.e.uiControls,
D.append(
P.e.cbDarkMode.parentNode/*the .input-with-label element*/,
F.helpButtonlets.create(
D.div(),
'Dark mode changes the colors of rendered SVG to ',
'make them more visible in dark-themed skins. ',
'This only changes (using CSS) how they are rendered, ',
'not any actual colors written in the script.',
D.br(), D.br(),
'In some color combinations, certain browsers might ',
'cause the SVG image to blur considerably with this ',
'setting enabled!'
)
)
);
////////////////////////////////////////////////////////////
// File drag/drop pikchr scripts into P.e.taContent.
// Adapted from: https://stackoverflow.com/a/58677161
const dropHighlight = P.e.taContent;
const dropEvents = {
drop: function(ev){
//ev.stopPropagation();
ev.preventDefault();
D.removeClass(dropHighlight, 'dragover');
const file = ev.dataTransfer.files[0];
if(file) {
const reader = new FileReader();
reader.addEventListener(
'load', function(e) {P.e.taContent.value = e.target.result}, false
);
reader.readAsText(file, "UTF-8");
}
},
dragenter: function(ev){
//ev.stopPropagation();
ev.preventDefault();
ev.dataTransfer.dropEffect = "copy";
D.addClass(dropHighlight, 'dragover');
//console.debug("dragenter");
},
dragover: function(ev){
//ev.stopPropagation();
ev.preventDefault();
//console.debug("dragover");
},
dragend: function(ev){
//ev.stopPropagation();
ev.preventDefault();
//console.debug("dragend");
},
dragleave: function(ev){
//ev.stopPropagation();
ev.preventDefault();
D.removeClass(dropHighlight, 'dragover');
//console.debug("dragleave");
}
};
/*
The idea here is to accept drops at multiple points or, ideally,
document.body, and apply them to P.e.taContent, but the precise
combination of event handling needed to pull this off is eluding
me.
*/
[P.e.taContent
//P.e.previewTarget,// works only until we drag over the SVG element!
//document.body
/* ideally we'd link only to document.body, but the events seem to
get out of whack, with dropleave being triggered
at unexpected points. */
].forEach(function(e){
Object.keys(dropEvents).forEach(
(k)=>e.addEventListener(k, dropEvents[k], true)
);
});
////////////////////////////////////////////////////////////
// If we start with content, get it in sync with the state
// generated by P.preview().
if(P.e.taContent.value/*was pre-filled server-side*/){
/* Fill our "response" state so that renderPreview() can work */
P.response.inputText = P.e.taContent.value;
P.response.raw = P.e.previewTarget.innerHTML;
P.renderPreview()/*it's already rendered, but this gets all
labels/headers in sync.*/;
}
}/*F.onPageLoad()*/);
/**
Updates the preview view based on the current preview mode and
error state.
*/
P.renderPreview = function f(){
if(!f.hasOwnProperty('rxNonce')){
f.rxNonce = /<!--.+-->\r?\n?/g /*pikchr nonce comments*/;
f.showMarkupAlignment = function(showIt){
P.e.markupAlignWrapper.classList[showIt ? 'remove' : 'add']('hidden');
};
f.getMarkupAlignmentClass = function(){
if(P.e.markupAlignCenter.checked) return ' center';
else if(P.e.markupAlignIndent.checked) return ' indent';
return '';
};
}
const preTgt = this.e.previewTarget;
if(this.response.isError){
D.append(D.clearElement(preTgt), D.parseHtml(P.response.raw));
D.addClass(preTgt, 'error');
this.e.previewModeLabel.innerText = "Error";
return;
}
D.removeClass(preTgt, 'error');
D.removeClass(this.e.previewCopyButton, 'disabled');
D.removeClass(this.e.markupAlignWrapper, 'hidden');
D.enable(this.e.previewModeToggle, this.e.markupAlignRadios);
let label;
switch(this.previewMode){
case 0:
label = "SVG";
f.showMarkupAlignment(false);
D.append(D.clearElement(preTgt), D.parseHtml(P.response.raw));
this.e.taPreviewText.value =
this.response.raw.replace(f.rxNonce, '')/*for copy button*/;
break;
case 1:
label = "Markdown";
f.showMarkupAlignment(true);
this.e.taPreviewText.value = [
'```pikchr'+f.getMarkupAlignmentClass(),
this.response.inputText.trim(), '```'
].join('\n');
D.append(D.clearElement(preTgt), this.e.taPreviewText);
break;
case 2:
label = "Fossil wiki";
f.showMarkupAlignment(true);
this.e.taPreviewText.value = [
'<verbatim type="pikchr',
f.getMarkupAlignmentClass(),
'">', this.response.inputText.trim(), '</verbatim>'
].join('');
D.append(D.clearElement(preTgt), this.e.taPreviewText);
break;
case 3:
label = "Raw SVG";
f.showMarkupAlignment(false);
this.e.taPreviewText.value = this.response.raw.replace(f.rxNonce, '');
D.append(D.clearElement(preTgt), this.e.taPreviewText);
break;
}
this.e.previewModeLabel.innerText = label;
};
/**
Fetches the preview from the server and updates the preview to
the rendered SVG content or error report.
*/
P.preview = function fp(){
if(!fp.hasOwnProperty('toDisable')){
fp.toDisable = [
/* input elements to disable during ajax operations */
this.e.btnSubmit, this.e.taContent,
this.e.cbAutoPreview, this.e.selectScript
/* handled separately: previewModeToggle, previewCopyButton,
markupAlignRadios */
];
fp.target = this.e.previewTarget;
fp.updateView = function(c,isError){
P.previewMode = 0;
P.response.raw = c;
P.response.isError = isError;
D.enable(fp.toDisable);
P.renderPreview();
};
}
D.disable(fp.toDisable, this.e.previewModeToggle, this.e.markupAlignRadios);
D.addClass(this.e.markupAlignWrapper, 'hidden');
D.addClass(this.e.previewCopyButton, 'disabled');
const content = this.e.taContent.value.trim();
this.response.raw = undefined;
this.response.inputText = content;
const sampleScript = fp.$_sampleScript;
delete fp.$_sampleScript;
if(sampleScript && sampleScript.cached){
fp.updateView(sampleScript.cached, false);
return this;
}
if(!content){
fp.updateView("No pikchr content!",true);
return this;
}
const self = this;
const fd = new FormData();
fd.append('ajax', true);
fd.append('content',content);
F.message(
"Fetching preview..."
).fetch('pikchrshow',{
payload: fd,
responseHeaders: 'x-pikchrshow-is-error',
onload: (r,isErrHeader)=>{
const isErr = +isErrHeader ? true : false;
if(!isErr && sampleScript){
sampleScript.cached = r;
}
fp.updateView(r,isErr);
F.message('Updated preview.');
},
onerror: (e)=>{
F.fetch.onerror(e);
fp.updateView("Error fetching preview: "+e, true);
}
});
return this;
}/*preview()*/;
/**
Predefined scripts. Each entry is an object:
{
name: required string,
code: optional code string. An entry with no code
is treated like a separator in the resulting
SELECT element (a disabled OPTION).
}
*/
P.predefinedPiks = [
{name: "-- Example Scripts --"},
/*
The following were imported from the pikchr test scripts:
https://fossil-scm.org/pikchr/dir/examples
*/
{name:"Cardinal headings",code:` linerad = 5px
C: circle "Center" rad 150%
circle "N" at 1.0 n of C; arrow from C to last chop ->
circle "NE" at 1.0 ne of C; arrow from C to last chop <-
circle "E" at 1.0 e of C; arrow from C to last chop <->
circle "SE" at 1.0 se of C; arrow from C to last chop ->
circle "S" at 1.0 s of C; arrow from C to last chop <-
circle "SW" at 1.0 sw of C; arrow from C to last chop <->
circle "W" at 1.0 w of C; arrow from C to last chop ->
circle "NW" at 1.0 nw of C; arrow from C to last chop <-
arrow from 2nd circle to 3rd circle chop
arrow from 4th circle to 3rd circle chop
arrow from SW to S chop <->
circle "ESE" at 2.0 heading 112.5 from Center \
thickness 150% fill lightblue radius 75%
arrow from Center to ESE thickness 150% <-> chop
arrow from ESE up 1.35 then to NE chop
line dashed <- from E.e to (ESE.x,E.y)
line dotted <-> thickness 50% from N to NW chop
`},{name:"Core object types",code:`AllObjects: [
# First row of objects
box "box"
box rad 10px "box (with" "rounded" "corners)" at 1in right of previous
circle "circle" at 1in right of previous
ellipse "ellipse" at 1in right of previous
# second row of objects
OVAL1: oval "oval" at 1in below first box
oval "(tall &" "thin)" "oval" width OVAL1.height height OVAL1.width \
at 1in right of previous
cylinder "cylinder" at 1in right of previous
file "file" at 1in right of previous
# third row shows line-type objects
dot "dot" above at 1in below first oval
line right from 1.8cm right of previous "lines" above
arrow right from 1.8cm right of previous "arrows" above
spline from 1.8cm right of previous \
go right .15 then .3 heading 30 then .5 heading 160 then .4 heading 20 \
then right .15
"splines" at 3rd vertex of previous
# The third vertex of the spline is not actually on the drawn
# curve. The third vertex is a control point. To see its actual
# position, uncomment the following line:
#dot color red at 3rd vertex of previous spline
# Draw various lines below the first line
line dashed right from 0.3cm below start of previous line
line dotted right from 0.3cm below start of previous
line thin right from 0.3cm below start of previous
line thick right from 0.3cm below start of previous
# Draw arrows with different arrowhead configurations below
# the first arrow
arrow <- right from 0.4cm below start of previous arrow
arrow <-> right from 0.4cm below start of previous
# Draw splines with different arrowhead configurations below
# the first spline
spline same from .4cm below start of first spline ->
spline same from .4cm below start of previous <-
spline same from .4cm below start of previous <->
] # end of AllObjects
# Label the whole diagram
text "Examples Of Pikchr Objects" big bold at .8cm above north of AllObjects
`},{name:"Swimlanes",code:` $laneh = 0.75
# Draw the lanes
down
box width 3.5in height $laneh fill 0xacc9e3
box same fill 0xc5d8ef
box same as first box
box same as 2nd box
line from 1st box.sw+(0.2,0) up until even with 1st box.n \
"Alan" above aligned
line from 2nd box.sw+(0.2,0) up until even with 2nd box.n \
"Betty" above aligned
line from 3rd box.sw+(0.2,0) up until even with 3rd box.n \
"Charlie" above aligned
line from 4th box.sw+(0.2,0) up until even with 4th box.n \
"Darlene" above aligned
# fill in content for the Alice lane
right
A1: circle rad 0.1in at end of first line + (0.2,-0.2) \
fill white thickness 1.5px "1"
arrow right 50%
circle same "2"
arrow right until even with first box.e - (0.65,0.0)
ellipse "future" fit fill white height 0.2 width 0.5 thickness 1.5px
A3: circle same at A1+(0.8,-0.3) "3" fill 0xc0c0c0
arrow from A1 to last circle chop "fork!" below aligned
# content for the Betty lane
B1: circle same as A1 at A1-(0,$laneh) "1"
arrow right 50%
circle same "2"
arrow right until even with first ellipse.w
ellipse same "future"
B3: circle same at A3-(0,$laneh) "3"
arrow right 50%
circle same as A3 "4"
arrow from B1 to 2nd last circle chop
# content for the Charlie lane
C1: circle same as A1 at B1-(0,$laneh) "1"
arrow 50%
circle same "2"
arrow right 0.8in "goes" "offline"
C5: circle same as A3 "5"
arrow right until even with first ellipse.w \
"back online" above "pushes 5" below "pulls 3 & 4" below
ellipse same "future"
# content for the Darlene lane
D1: circle same as A1 at C1-(0,$laneh) "1"
arrow 50%
circle same "2"
arrow right until even with C5.w
circle same "5"
arrow 50%
circle same as A3 "6"
arrow right until even with first ellipse.w
ellipse same "future"
D3: circle same as B3 at B3-(0,2*$laneh) "3"
arrow 50%
circle same "4"
arrow from D1 to D3 chop
`}
];
})(window.fossil);
|