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
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
|
/* This module contains javascript needed to render timeline graphs in Fossil.
**
** There can be multiple graphs on a single webpage, but this script is only
** loaded once.
**
** Prior to sourcing this script, there should be a separate
** <script type='application/json' id='timeline-data-NN'> for each graph,
** each containing JSON like this:
**
** { "iTableId": INTEGER, // Table sequence number (NN)
** "circleNodes": BOOLEAN, // True for circle nodes. False for squares
** "showArrowheads": BOOLEAN, // True for arrowheads. False to omit
** "iRailPitch": INTEGER, // Spacing between vertical lines (px)
** "nomo": BOOLEAN, // True to join merge lines with rails
** "iTopRow": INTEGER, // Index of top-most row in the graph
** "omitDescenders": BOOLEAN, // Omit ancestor lines off bottom of screen
** "fileDiff": BOOLEAN, // True for file diff. False for check-in
** "scrollToSelect": BOOLEAN, // Scroll to selection on first render
** "nrail": INTEGER, // Number of vertical "rails"
** "baseUrl": TEXT, // Top-level URL
** "dwellTimeout": INTEGER, // Tooltip show delay in milliseconds
** "closeTimeout": INTEGER, // Tooltip close delay in milliseconds
** "hashDigits": INTEGER, // Limit of tooltip hashes ("hash-digits")
** "rowinfo": ROWINFO-ARRAY }
**
** The rowinfo field is an array of structures, one per entry in the timeline,
** where each structure has the following fields:
**
** id: The id of the <div> element for the row. This is an integer.
** to get an actual id, prepend "m" to the integer. The top node
** is iTopRow and numbers increase moving down the timeline.
** bg: The background color for this row
** r: The "rail" that the node for this row sits on. The left-most
** rail is 0 and the number increases to the right.
** d: If exists and true then there is a "descender" - an arrow
** coming from the bottom of the page straight up to this node.
** mo: "merge-out". If it exists, this is the rail position
** for the upward portion of a merge arrow. The merge arrow goes as
** a solid normal merge line up to the row identified by "mu" and
** then as a dashed cherrypick merge line up further to "cu".
** If this value is omitted if there are no merge children.
** mu: The id of the row which is the top of the merge-out arrow.
** Only exists if "mo" exists.
** cu: Extend the mu merge arrow up to this row as a cherrypick
** merge line, if this value exists.
** u: Draw a thick child-line out of the top of this node and up to
** the node with an id equal to this value. 0 if it is straight to
** the top of the page, -1 if there is no thick-line riser.
** f: 0x01: a leaf node.
** au: An array of integers that define thick-line risers for branches.
** The integers are in pairs. For each pair, the first integer is
** is the rail on which the riser should run and the second integer
** is the id of the node upto which the riser should run. If there
** are no risers, this array does not exist.
** mi: "merge-in". An array of integer rail positions from which
** merge arrows should be drawn into this node. If the value is
** negative, then the rail position is -1-mi[] and a thin merge-arrow
** descender is drawn to the bottom of the screen. This array is
** omitted if there are no inbound merges.
** ci: "cherrypick-in". Like "mi" except for cherrypick merges.
** omitted if there are no cherrypick merges.
** h: The artifact hash of the object being graphed
*/
/* The amendCss() function does a one-time change to the CSS to account
** for the "circleNodes" and "showArrowheads" settings. Do this change
** only once, even if there are multiple graphs being rendered.
*/
var amendCssOnce = 1; // Only change the CSS one time
function amendCss(circleNodes,showArrowheads){
if( !amendCssOnce ) return;
var css = "";
if( circleNodes ){
css += ".tl-node, .tl-node:after { border-radius: 50%; }";
}
if( !showArrowheads ){
css += ".tl-arrow.u { display: none; }";
}
if( css!=="" ){
var style = document.createElement("style");
style.textContent = css;
document.querySelector("head").appendChild(style);
}
amendCssOnce = 0;
}
/* The <span> object that holds the tooltip */
var tooltipObj = document.createElement("span");
tooltipObj.className = "tl-tooltip";
tooltipObj.style.display = "none";
document.getElementsByClassName("content")[0].appendChild(tooltipObj);
tooltipObj.onmouseenter = function(){
/* Hold the tooltip constant as long as the mouse is over the tooltip.
** In other words, do not let any of the timers changes the tooltip while
** the mouse is directly over the tooltip. This makes it easier for the
** user to move over top of the "copy-button" or the hyperlink to the
** /info page. */
stopCloseTimer();
stopDwellTimer();
tooltipInfo.ixHover = tooltipInfo.ixActive;
}
tooltipObj.onmouseleave = function(){
if (tooltipInfo.ixActive != -1) resumeCloseTimer();
};
/* State information for the tooltip popup and its timers */
window.tooltipInfo = {
dwellTimeout: 250, /* The tooltip dwell timeout. */
closeTimeout: 3000, /* The tooltip close timeout. */
hashDigits: 16, /* Limit of tooltip hashes ("hash-digits"). */
idTimer: 0, /* The tooltip dwell timer id */
idTimerClose: 0, /* The tooltip close timer id */
ixHover: -1, /* The mouse is over a thick riser arrow for
** tx.rowinfo[ixHover]. Or -2 when the mouse is
** over a graph node. Or -1 when the mouse is not
** over anything. */
ixActive: -1, /* The item shown in the tooltip is tx.rowinfo[ixActive].
** ixActive is -1 if the tooltip is not visible */
nodeHover: null, /* Graph node under mouse when ixHover==-2 */
idNodeActive: 0, /* Element ID of the graph node with the tooltip. */
posX: 0, posY: 0 /* The last mouse position. */
};
/* Functions used to control the tooltip popup and its timer */
function onKeyDown(event){ /* Hide the tooltip when ESC key pressed */
var key = event.which || event.keyCode;
if( key==27 ){
event.stopPropagation();
hideGraphTooltip();
}
}
function hideGraphTooltip(){ /* Hide the tooltip */
document.removeEventListener('keydown',onKeyDown,/* useCapture == */true);
stopCloseTimer();
tooltipObj.style.display = "none";
tooltipInfo.ixActive = -1;
tooltipInfo.idNodeActive = 0;
}
window.onpagehide = hideGraphTooltip;
function stopDwellTimer(){
if(tooltipInfo.idTimer!=0){
clearTimeout(tooltipInfo.idTimer);
tooltipInfo.idTimer = 0;
}
}
function resumeCloseTimer(){
/* This timer must be stopped explicitly to reset the elapsed timeout. */
if(tooltipInfo.idTimerClose==0 && tooltipInfo.closeTimeout>0) {
tooltipInfo.idTimerClose = setTimeout(function(){
tooltipInfo.idTimerClose = 0;
hideGraphTooltip();
},tooltipInfo.closeTimeout);
}
}
function stopCloseTimer(){
if(tooltipInfo.idTimerClose!=0){
clearTimeout(tooltipInfo.idTimerClose);
tooltipInfo.idTimerClose = 0;
}
}
/* Construct that graph corresponding to the timeline-data-N object that
** is passed in by the tx parameter */
function TimelineGraph(tx){
var topObj = document.getElementById("timelineTable"+tx.iTableId);
amendCss(tx.circleNodes, tx.showArrowheads);
tooltipInfo.dwellTimeout = tx.dwellTimeout
tooltipInfo.closeTimeout = tx.closeTimeout
tooltipInfo.hashDigits = tx.hashDigits
topObj.onclick = clickOnGraph
topObj.ondblclick = dblclickOnGraph
topObj.onmousemove = function(e) {
var ix = findTxIndex(e);
topObj.style.cursor = (ix<0) ? "" : "pointer"
mouseOverGraph(e,ix,null);
};
topObj.onmouseleave = function(e) {
/* Hide the tooltip if the mouse is outside the "timelineTableN" element,
** and outside the tooltip. */
if(e.relatedTarget && e.relatedTarget != tooltipObj){
tooltipInfo.ixHover = -1;
hideGraphTooltip();
stopDwellTimer();
stopCloseTimer();
}
};
function mouseOverNode(e){ /* Invoked by mousemove events over a graph node */
e.stopPropagation()
mouseOverGraph(e,-2,this)
}
/* Combined mousemove handler for graph nodes and rails. */
function mouseOverGraph(e,ix,node){
stopDwellTimer(); // Mouse movement: reset the dwell timer.
var ownTooltip = // Check if the hovered element already has the tooltip.
(ix>=0 && ix==tooltipInfo.ixActive) ||
(ix==-2 && tooltipInfo.idNodeActive==node.id);
if(ownTooltip) stopCloseTimer(); // ownTooltip: clear the close timer.
else resumeCloseTimer(); // !ownTooltip: resume the close timer.
tooltipInfo.ixHover = ix;
tooltipInfo.nodeHover = node;
tooltipInfo.posX = e.clientX;
tooltipInfo.posY = e.clientY;
if(ix!=-1 && !ownTooltip && tooltipInfo.dwellTimeout>0){ // Go dwell timer.
tooltipInfo.idTimer = setTimeout(function(){
tooltipInfo.idTimer = 0;
stopCloseTimer();
showGraphTooltip();
},tooltipInfo.dwellTimeout);
}
}
var canvasDiv;
var railPitch;
var mergeOffset;
var node, arrow, arrowSmall, line, mArrow, mLine, wArrow, wLine;
function initGraph(){
var parent = topObj.rows[0].cells[1];
parent.style.verticalAlign = "top";
canvasDiv = document.createElement("div");
canvasDiv.className = "tl-canvas";
canvasDiv.style.position = "absolute";
parent.appendChild(canvasDiv);
var elems = {};
var elemClasses = [
"rail", "mergeoffset", "node", "arrow u", "arrow u sm", "line",
"arrow merge r", "line merge", "arrow warp", "line warp",
"line cherrypick", "line dotted"
];
for( var i=0; i<elemClasses.length; i++ ){
var cls = elemClasses[i];
var elem = document.createElement("div");
elem.className = "tl-" + cls;
if( cls.indexOf("line")==0 ) elem.className += " v";
canvasDiv.appendChild(elem);
var k = cls.replace(/\s/g, "_");
var r = elem.getBoundingClientRect();
var w = Math.round(r.right - r.left);
var h = Math.round(r.bottom - r.top);
elems[k] = {w: w, h: h, cls: cls};
}
node = elems.node;
arrow = elems.arrow_u;
arrowSmall = elems.arrow_u_sm;
line = elems.line;
mArrow = elems.arrow_merge_r;
mLine = elems.line_merge;
cpLine = elems.line_cherrypick;
wArrow = elems.arrow_warp;
wLine = elems.line_warp;
dotLine = elems.line_dotted;
var minRailPitch = Math.ceil((node.w+line.w)/2 + mArrow.w + 1);
if( window.innerWidth<400 ){
railPitch = minRailPitch;
}else{
if( tx.iRailPitch>0 ){
railPitch = tx.iRailPitch;
}else{
railPitch = elems.rail.w;
railPitch -= Math.floor((tx.nrail-1)*(railPitch-minRailPitch)/21);
}
railPitch = Math.max(railPitch, minRailPitch);
}
if( tx.nomo ){
mergeOffset = 0;
}else{
mergeOffset = railPitch-minRailPitch-mLine.w;
mergeOffset = Math.min(mergeOffset, elems.mergeoffset.w);
mergeOffset = mergeOffset>0 ? mergeOffset + line.w/2 : 0;
}
var canvasWidth = (tx.nrail-1)*railPitch + node.w;
canvasDiv.style.width = canvasWidth + "px";
canvasDiv.style.position = "relative";
}
function drawBox(cls,color,x0,y0,x1,y1){
var n = document.createElement("div");
x0 = Math.floor(x0);
y0 = Math.floor(y0);
x1 = x1 || x1===0 ? Math.floor(x1) : x0;
y1 = y1 || y1===0 ? Math.floor(y1) : y0;
if( x0>x1 ){ var t=x0; x0=x1; x1=t; }
if( y0>y1 ){ var t=y0; y0=y1; y1=t; }
var w = x1-x0;
var h = y1-y0;
n.style.position = "absolute";
n.style.left = x0+"px";
n.style.top = y0+"px";
if( w ) n.style.width = w+"px";
if( h ) n.style.height = h+"px";
if( color ) n.style.backgroundColor = color;
n.className = "tl-"+cls;
canvasDiv.appendChild(n);
return n;
}
function absoluteY(obj){
var y = 0;
do{
y += obj.offsetTop;
}while( obj = obj.offsetParent );
return y;
}
function absoluteX(obj){
var x = 0;
do{
x += obj.offsetLeft;
}while( obj = obj.offsetParent );
return x;
}
function miLineY(p){
return p.y + node.h - mLine.w - 1;
}
function drawLine(elem,color,x0,y0,x1,y1){
var cls = elem.cls + " ";
if( x1===null ){
x1 = x0+elem.w;
cls += "v";
}else{
y1 = y0+elem.w;
cls += "h";
}
return drawBox(cls,color,x0,y0,x1,y1);
}
function drawUpArrow(from,to,color,id){
var y = to.y + node.h;
var arrowSpace = from.y - y + (!from.id || from.r!=to.r ? node.h/2 : 0);
var arw = arrowSpace < arrow.h*1.5 ? arrowSmall : arrow;
var x = to.x + (node.w-line.w)/2;
var y0 = from.y + node.h/2;
var y1 = Math.ceil(to.y + node.h + arw.h/2);
var n = drawLine(line,color,x,y0,null,y1);
addToolTip(n,id)
x = to.x + (node.w-arw.w)/2;
n = drawBox(arw.cls,null,x,y);
if(color) n.style.borderBottomColor = color;
addToolTip(n,id)
}
function drawDotted(from,to,color,id){
var x = to.x + (node.w-line.w)/2;
var y0 = from.y + node.h/2;
var y1 = Math.ceil(to.y + node.h);
var n = drawLine(dotLine,null,x,y0,null,y1)
if( color ) n.style.borderColor = color
addToolTip(n,id)
}
function addToolTip(n,id){
if( id ) n.setAttribute("data-ix",id-tx.iTopRow)
}
/* Draw thin horizontal or vertical lines representing merges */
function drawMergeLine(x0,y0,x1,y1){
drawLine(mLine,null,x0,y0,x1,y1);
}
function drawCherrypickLine(x0,y0,x1,y1){
drawLine(cpLine,null,x0,y0,x1,y1);
}
/* Draw an arrow representing an in-bound merge from the "rail"-th rail
** over to the node of "p". Make it a checkpoint merge is "isCP" is true */
function drawMergeArrow(p,rail,isCP){
var x0 = rail*railPitch + node.w/2;
if( rail in mergeLines ){
x0 += mergeLines[rail];
if( p.r<rail ) x0 += mLine.w;
}else{
x0 += (p.r<rail ? -1 : 1)*line.w/2;
}
var x1 = mArrow.w ? mArrow.w/2 : -node.w/2;
x1 = p.x + (p.r<rail ? node.w + Math.ceil(x1) : -x1);
var y = miLineY(p);
var x = p.x + (p.r<rail ? node.w : -mArrow.w);
var cls;
if( isCP ){
drawCherrypickLine(x0,y,x1,null);
cls = "arrow cherrypick " + (p.r<rail ? "l" : "r");
}else{
drawMergeLine(x0,y,x1,null);
cls = "arrow merge " + (p.r<rail ? "l" : "r");
}
drawBox(cls,null,x,y+(mLine.w-mArrow.h)/2);
}
function drawNode(p, btm){
if( p.bg ){
var e = document.getElementById("mc"+p.id);
if(e) e.style.backgroundColor = p.bg;
e = document.getElementById("md"+p.id);
if(e) e.style.backgroundColor = p.bg;
}
if( p.r<0 ) return;
if( p.u>0 ) drawUpArrow(p,tx.rowinfo[p.u-tx.iTopRow],p.fg,p.id);
if( p.sb>0 ) drawDotted(p,tx.rowinfo[p.sb-tx.iTopRow],p.fg,p.id);
var cls = node.cls;
if( p.hasOwnProperty('mi') && p.mi.length ) cls += " merge";
if( p.f&2 ) cls += " closed-leaf";
else if( p.f&1 ) cls += " leaf";
var n = drawBox(cls,p.bg,p.x,p.y);
n.id = "tln"+p.id;
n.onclick = clickOnNode;
n.ondblclick = dblclickOnNode;
n.onmousemove = mouseOverNode;
n.style.zIndex = 10;
if( p.f&2 ){
var pt1 = 0;
var pt2 = 100;
if( tx.circleNodes ){
pt1 = 14;
pt2 = 86;
}
n.innerHTML = "<svg width='100%' height='100%'viewbox='0 0 100 100'>"
+ `<path d='M ${pt1},${pt1} L ${pt2},${pt2} M ${pt1},${pt2} L ${pt2},${pt1}'`
+ " stroke='currentcolor' stroke-width='13'/>"
+ "</svg>";
}
if( !tx.omitDescenders ){
if( p.u==0 ){
if( p.hasOwnProperty('mo') && p.r==p.mo ){
var ix = p.hasOwnProperty('cu') ? p.cu : p.mu;
var top = tx.rowinfo[ix-tx.iTopRow]
drawUpArrow(p,{x: p.x, y: top.y-node.h}, p.fg, p.id);
}else if( p.y>100 ){
drawUpArrow(p,{x: p.x, y: p.y-50}, p.fg, p.id);
}else{
drawUpArrow(p,{x: p.x, y: 0},p.fg, p.id);
}
}
if( p.hasOwnProperty('d') ){
if( p.y + 150 >= btm ){
drawUpArrow({x: p.x, y: btm - node.h/2},p,p.fg,p.id);
}else{
drawUpArrow({x: p.x, y: p.y+50},p,p.fg,p.id);
drawDotted({x: p.x, y: p.y+63},{x: p.x, y: p.y+50-node.h/2},p.fg,p.id);
}
}
}
if( p.hasOwnProperty('mo') ){
var x0 = p.x + node.w/2;
var x1 = p.mo*railPitch + node.w/2;
var u = tx.rowinfo[p.mu-tx.iTopRow];
var mtop = u;
if( p.hasOwnProperty('cu') ){
mtop = tx.rowinfo[p.cu-tx.iTopRow];
}
var y1 = miLineY(u);
if( p.u<=0 || p.mo!=p.r ){
if( p.u==0 && p.mo==p.r ){
mergeLines[p.mo] = mtop.r<p.r ? -mergeOffset-mLine.w : mergeOffset;
}else{
mergeLines[p.mo] = -mLine.w/2;
}
x1 += mergeLines[p.mo]
var y0 = p.y+2;
var isCP = p.hasOwnProperty('cu');
if( p.mu==p.id ){
/* Special case: The merge riser already exists. Only draw the
/* horizontal line or arrow going from the node out to the riser. */
var dx = x1<x0 ? mArrow.w : -mArrow.w;
if( isCP ){
drawCherrypickLine(x0,y0,x1+dx,null);
cls = "arrow cherrypick " + (x1<x0 ? "l" : "r");
}else{
drawMergeLine(x0,y0,x1+dx,null);
cls = "arrow merge " + (x1<x0 ? "l" : "r");
}
if( !isCP || p.mu==p.cu ){
dx = x1<x0 ? mLine.w : -(mArrow.w + mLine.w/2);
drawBox(cls,null,x1+dx,y0+(mLine.w-mArrow.h)/2);
}
y1 = y0;
}else{
drawMergeLine(x0,y0,x1+(x0<x1 ? mLine.w : 0),null);
drawMergeLine(x1,y0+mLine.w,null,y1);
}
if( isCP && p.cu!=p.id ){
var u2 = tx.rowinfo[p.cu-tx.iTopRow];
var y2 = miLineY(u2);
drawCherrypickLine(x1,y1,null,y2);
}
}else if( mergeOffset ){
mergeLines[p.mo] = mtop.r<p.r ? -mergeOffset-mLine.w : mergeOffset;
x1 += mergeLines[p.mo];
if( p.mu<p.id ){
drawMergeLine(x1,p.y+node.h/2,null,y1);
}
if( p.hasOwnProperty('cu') ){
var u2 = tx.rowinfo[p.cu-tx.iTopRow];
var y2 = miLineY(u2);
drawCherrypickLine(x1,y1,null,y2);
}
}else{
delete mergeLines[p.mo];
}
}
if( p.hasOwnProperty('au') ){
for( var i=0; i<p.au.length; i+=2 ){
var rail = p.au[i];
var x0 = p.x + node.w/2;
var x1 = rail*railPitch + (node.w-line.w)/2;
if( x0<x1 ){
x0 = Math.ceil(x0);
x1 += line.w;
}
var y0 = p.y + (node.h-line.w)/2;
var u = tx.rowinfo[p.au[i+1]-tx.iTopRow];
if( u.id<p.id ){
// normal thick up-arrow
drawLine(line,u.fg,x0,y0,x1,null);
drawUpArrow(p,u,u.fg,u.id);
}else{
// timewarp: The child node occurs before the parent
var y1 = u.y + (node.h-line.w)/2;
var n = drawLine(wLine,u.fg,x0,y0,x1,null);
addToolTip(n,u.id)
n = drawLine(wLine,u.fg,x1-line.w,y0,null,y1+line.w);
addToolTip(n,u.id)
n = drawLine(wLine,u.fg,x1,y1,u.x-wArrow.w/2,null);
addToolTip(n,u.id)
var x = u.x-wArrow.w;
var y = u.y+(node.h-wArrow.h)/2;
n = drawBox(wArrow.cls,null,x,y);
addToolTip(n,u.id)
if( u.fg ) n.style.borderLeftColor = u.fg;
}
}
}
if( p.hasOwnProperty('mi') ){
for( var i=0; i<p.mi.length; i++ ){
var rail = p.mi[i];
if( rail<0 ){
rail = -1-rail;
mergeLines[rail] = -mLine.w/2;
var x = rail*railPitch + (node.w-mLine.w)/2;
var y = miLineY(p);
drawMergeLine(x,y,null,mergeBtm[rail]);
mergeBtm[rail] = y;
}
drawMergeArrow(p,rail,0);
}
}
if( p.hasOwnProperty('ci') ){
for( var i=0; i<p.ci.length; i++ ){
var rail = p.ci[i];
if( rail<0 ){
rail = -rail;
mergeLines[rail] = -mLine.w/2;
var x = rail*railPitch + (node.w-mLine.w)/2;
var y = miLineY(p);
drawCherrypickLine(x,y,null,mergeBtm[rail]);
mergeBtm[rail] = y;
}
drawMergeArrow(p,rail,1);
}
}
}
var mergeLines;
var mergeBtm = new Array;
function renderGraph(){
mergeLines = {};
canvasDiv.innerHTML = "";
var canvasY = absoluteY(canvasDiv);
for(var i=0; i<tx.rowinfo.length; i++ ){
var e = document.getElementById("m"+tx.rowinfo[i].id);
tx.rowinfo[i].y = absoluteY(e) - canvasY;
tx.rowinfo[i].x = tx.rowinfo[i].r*railPitch;
}
var tlBtm = document.getElementById(tx.bottomRowId);
if( tlBtm.offsetHeight<node.h ){
tlBtm.style.height = node.h + "px";
}
var btm = absoluteY(tlBtm) - canvasY + tlBtm.offsetHeight;
for( var i=0; i<tx.nrail; i++) mergeBtm[i] = btm;
for( var i=tx.rowinfo.length-1; i>=0; i-- ){
drawNode(tx.rowinfo[i], btm);
}
}
var selRow;
function clickOnNode(e){
hideGraphTooltip()
var p = tx.rowinfo[parseInt(this.id.match(/\d+$/)[0], 10)-tx.iTopRow];
if( !selRow ){
selRow = p;
this.className += " sel";
canvasDiv.className += " sel";
}else if( selRow==p ){
selRow = null;
this.className = this.className.replace(" sel", "");
canvasDiv.className = canvasDiv.className.replace(" sel", "");
}else{
if( tx.fileDiff ){
location.href=tx.baseUrl + "/fdiff?v1="+selRow.h+"&v2="+p.h;
}else{
var href = tx.baseUrl + "/vdiff?from="+selRow.h+"&to="+p.h;
let params = (new URL(document.location)).searchParams;
if(params && typeof params === "object"){
/* When called from /timeline page, If chng=str was specified in the
** QueryString, specify glob=str on the /vdiff page */
let glob = params.get("chng");
if( !glob ){
/* When called from /vdiff page, keep the glob= QueryString if
** present. */
glob = params.get("glob");
}
if( glob ){
href += "&glob=" + glob;
}
}
location.href = href;
}
}
e.stopPropagation()
}
function dblclickOnNode(e){
var p = tx.rowinfo[parseInt(this.id.match(/\d+$/)[0], 10)-tx.iTopRow];
window.location.href = tx.baseUrl+"/info/"+p.h
e.stopPropagation()
}
function findTxIndex(e){
if( !tx.rowinfo ) return -1;
/* Look at all the graph elements. If any graph elements that is near
** the click-point "e" and has a "data-ix" attribute, then return
** the value of that attribute. Otherwise return -1 */
var x = e.clientX + window.pageXOffset - absoluteX(canvasDiv);
var y = e.clientY + window.pageYOffset - absoluteY(canvasDiv);
var aNode = canvasDiv.childNodes
var nNode = aNode.length;
var i;
for(i=0;i<nNode;i++){
var n = aNode[i]
if( !n.hasAttribute("data-ix") ) continue;
if( x<n.offsetLeft-5 ) continue;
if( x>n.offsetLeft+n.offsetWidth+5 ) continue;
if( y<n.offsetTop-5 ) continue;
if( y>n.offsetTop+n.offsetHeight ) continue;
return n.getAttribute("data-ix")
}
return -1
}
/* Compute the hyperlink for the branch graph for tx.rowinfo[ix] */
function branchHyperlink(ix){
var br = tx.rowinfo[ix].br
var dest = tx.baseUrl + "/timeline?r=" + encodeURIComponent(br)
dest += tx.fileDiff ? "&m&cf=" : "&m&c="
dest += encodeURIComponent(tx.rowinfo[ix].h)
return dest
}
function clickOnGraph(e){
stopCloseTimer();
stopDwellTimer();
tooltipInfo.ixHover = findTxIndex(e);
tooltipInfo.posX = e.clientX;
tooltipInfo.posY = e.clientY;
showGraphTooltip();
}
function showGraphTooltip(){
var html = null
var ix = -1
if( tooltipInfo.ixHover==-2 ){
ix = parseInt(tooltipInfo.nodeHover.id.match(/\d+$/)[0],10)-tx.iTopRow
var h = tx.rowinfo[ix].h
var dest = tx.baseUrl + "/info/" + h
h = h.slice(0,tooltipInfo.hashDigits); // Assume single-byte characters.
if( tx.fileDiff ){
html = "artifact <a id=\"tooltip-link\" href=\""+dest+"\">"+h+"</a>"
}else{
html = "check-in <a id=\"tooltip-link\" href=\""+dest+"\">"+h+"</a>"
}
tooltipInfo.ixActive = -2;
tooltipInfo.idNodeActive = tooltipInfo.nodeHover.id;
}else if( tooltipInfo.ixHover>=0 ){
ix = tooltipInfo.ixHover
var br = tx.rowinfo[ix].br
var dest = branchHyperlink(ix)
var hbr = br.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
html = "branch <a id=\"tooltip-link\" href=\""+dest+"\">"+hbr+"</a>"
tooltipInfo.ixActive = ix;
tooltipInfo.idNodeActive = 0;
}
if( html ){
/* Setup while hidden, to ensure proper dimensions. */
var s = getComputedStyle(document.body)
if( tx.rowinfo[ix].bg.length ){
tooltipObj.style.backgroundColor = tx.rowinfo[ix].bg
}else{
tooltipObj.style.backgroundColor = s.getPropertyValue('background-color')
}
tooltipObj.style.borderColor =
tooltipObj.style.color = s.getPropertyValue('color')
tooltipObj.style.visibility = "hidden"
tooltipObj.innerHTML = html
tooltipObj.insertBefore(makeCopyButton("tooltip-link",0,0),
tooltipObj.childNodes[1]);
tooltipObj.style.display = "inline"
tooltipObj.style.position = "absolute"
var x = tooltipInfo.posX + 4 + window.pageXOffset
- absoluteX(tooltipObj.offsetParent)
tooltipObj.style.left = x+"px"
var y = tooltipInfo.posY + window.pageYOffset
- tooltipObj.clientHeight - 4
- absoluteY(tooltipObj.offsetParent)
tooltipObj.style.top = y+"px"
tooltipObj.style.visibility = "visible"
document.addEventListener('keydown',onKeyDown,/* useCapture == */true);
}else{
hideGraphTooltip()
}
}
function dblclickOnGraph(e){
var ix = findTxIndex(e);
hideGraphTooltip()
if( ix>=0 ){
var dest = branchHyperlink(ix)
window.location.href = dest
}
}
function changeDisplay(selector,value){
var x = document.getElementsByClassName(selector);
var n = x.length;
for(var i=0; i<n; i++) {x[i].style.display = value;}
}
function changeDisplayById(id,value){
var x = document.getElementById(id);
if(x) x.style.display=value;
}
function toggleDetail(evt){
/* Ignore clicks to hyperlinks and other "click-responsive" HTML elements.
** This click-handler is set for <SPAN> elements with the CSS class names
** "timelineEllipsis" and "timelineCompactComment", which are part of the
** "Compact" and "Simple" views. */
var xClickyHTML = /^(?:A|AREA|BUTTON|INPUT|LABEL|SELECT|TEXTAREA|DETAILS)$/;
if( xClickyHTML.test(evt.target.tagName) ) return;
var id = parseInt(this.getAttribute('data-id'))
var x = document.getElementById("detail-"+id);
if( x.style.display=="inline" ){
x.style.display="none";
document.getElementById("ellipsis-"+id).textContent = "...";
changeDisplayById("links-"+id,"none");
}else{
x.style.display="inline";
document.getElementById("ellipsis-"+id).textContent = "←";
changeDisplayById("links-"+id,"inline");
}
checkHeight();
}
function scrollToSelected(){
var x = document.getElementsByClassName('timelineSelected');
if(x[0]){
var h = window.innerHeight;
var y = absoluteY(x[0]) - h/2;
if( y>0 ) window.scrollTo(0, y);
}
}
if( tx.rowinfo ){
var lastRow =
document.getElementById("m"+tx.rowinfo[tx.rowinfo.length-1].id);
var lastY = 0;
function checkHeight(){
var h = absoluteY(lastRow);
if( h!=lastY ){
renderGraph();
lastY = h;
}
setTimeout(checkHeight, 1000);
}
initGraph();
checkHeight();
}else{
function checkHeight(){}
}
if( tx.scrollToSelect ){
scrollToSelected();
}
/* Set the onclick= attributes for elements of the "Compact" and
** "Simple" views so that clicking turns the details on and off.
*/
var lx = topObj.getElementsByClassName('timelineEllipsis');
var i;
for(i=0; i<lx.length; i++){
if( lx[i].hasAttribute('data-id') ){
lx[i].addEventListener('click',toggleDetail);
}
}
lx = topObj.getElementsByClassName('timelineCompactComment');
for(i=0; i<lx.length; i++){
if( lx[i].hasAttribute('data-id') ){
lx[i].addEventListener('click',toggleDetail);
}
}
if( window.innerWidth<400 ){
/* On narrow displays, shift the date from the first column to the
** third column, to make the first column narrower */
lx = topObj.getElementsByClassName('timelineDateRow');
for(i=0; i<lx.length; i++){
var rx = lx[i];
if( rx.getAttribute('data-reordered') ) break;
rx.setAttribute('data-reordered',1);
rx.appendChild(rx.firstChild);
rx.insertBefore(rx.childNodes[1],rx.firstChild);
}
/* Do not show the HH:MM timestamps on very narrow displays
** as they take up too much horizontal space. */
lx = topObj.getElementsByClassName('timelineHistLink');
for(i=0; i<lx.length; i++){
var rx = lx[i];
rx.style.display="none";
}
}
}
/* Look for all timeline-data-NN objects. Load each one and draw
** a graph for each one.
*/
(function(){
var i;
for(i=0; 1; i++){
var dataObj = document.getElementById("timeline-data-"+i);
if(!dataObj) break;
var txJson = dataObj.textContent || dataObj.innerText;
var tx = JSON.parse(txJson);
TimelineGraph(tx);
}
}());
|