Fossil SCM

fossil-scm / src / graph.js
Blame History Raw 1236 lines
1
/* This module contains javascript needed to render timeline graphs in Fossil.
2
**
3
** There can be multiple graphs on a single webpage, but this script is only
4
** loaded once.
5
**
6
** Prior to sourcing this script, there should be a separate
7
** <script type='application/json' id='timeline-data-NN'> for each graph,
8
** each containing JSON like this:
9
**
10
** { "iTableId": INTEGER, // Table sequence number (NN)
11
** "circleNodes": BOOLEAN, // True for circle nodes. False for squares
12
** "showArrowheads": BOOLEAN, // True for arrowheads. False to omit
13
** "iRailPitch": INTEGER, // Spacing between vertical lines (px)
14
** "nomo": BOOLEAN, // True to join merge lines with rails
15
** "iTopRow": INTEGER, // Index of top-most row in the graph
16
** "omitDescenders": BOOLEAN, // Omit ancestor lines off bottom of screen
17
** "fileDiff": BOOLEAN, // True for file diff. False for check-in
18
** "scrollToSelect": BOOLEAN, // Scroll to selection on first render
19
** "nrail": INTEGER, // Number of vertical "rails"
20
** "baseUrl": TEXT, // Top-level URL
21
** "dwellTimeout": INTEGER, // Tooltip show delay in milliseconds
22
** "closeTimeout": INTEGER, // Tooltip close delay in milliseconds
23
** "hashDigits": INTEGER, // Limit of tooltip hashes ("hash-digits")
24
** "rowinfo": ROWINFO-ARRAY }
25
**
26
** The rowinfo field is an array of structures, one per entry in the timeline,
27
** where each structure has the following fields:
28
**
29
** id: The id of the <div> element for the row. This is an integer.
30
** to get an actual id, prepend "m" to the integer. The top node
31
** is iTopRow and numbers increase moving down the timeline.
32
** bg: The background color for this row
33
** r: The "rail" that the node for this row sits on. The left-most
34
** rail is 0 and the number increases to the right.
35
** d: If exists and true then there is a "descender" - an arrow
36
** coming from the bottom of the page straight up to this node.
37
** mo: "merge-out". If it exists, this is the rail position
38
** for the upward portion of a merge arrow. The merge arrow goes as
39
** a solid normal merge line up to the row identified by "mu" and
40
** then as a dashed cherrypick merge line up further to "cu".
41
** If this value is omitted if there are no merge children.
42
** mu: The id of the row which is the top of the merge-out arrow.
43
** Only exists if "mo" exists.
44
** cu: Extend the mu merge arrow up to this row as a cherrypick
45
** merge line, if this value exists.
46
** u: Draw a thick child-line out of the top of this node and up to
47
** the node with an id equal to this value. 0 if it is straight to
48
** the top of the page, -1 if there is no thick-line riser.
49
** f: 0x01: a leaf node.
50
** au: An array of integers that define thick-line risers for branches.
51
** The integers are in pairs. For each pair, the first integer is
52
** is the rail on which the riser should run and the second integer
53
** is the id of the node upto which the riser should run. If there
54
** are no risers, this array does not exist.
55
** mi: "merge-in". An array of integer rail positions from which
56
** merge arrows should be drawn into this node. If the value is
57
** negative, then the rail position is -1-mi[] and a thin merge-arrow
58
** descender is drawn to the bottom of the screen. This array is
59
** omitted if there are no inbound merges.
60
** ci: "cherrypick-in". Like "mi" except for cherrypick merges.
61
** omitted if there are no cherrypick merges.
62
** h: The artifact hash of the object being graphed
63
*/
64
/* The amendCss() function does a one-time change to the CSS to account
65
** for the "circleNodes" and "showArrowheads" settings. Do this change
66
** only once, even if there are multiple graphs being rendered.
67
*/
68
var amendCssOnce = 1; // Only change the CSS one time
69
function amendCss(circleNodes,showArrowheads){
70
if( !amendCssOnce ) return;
71
var css = "";
72
if( circleNodes ){
73
css += ".tl-node, .tl-node:after { border-radius: 50%; }";
74
}
75
if( !showArrowheads ){
76
css += ".tl-arrow.u { display: none; }";
77
}
78
if( css!=="" ){
79
var style = document.createElement("style");
80
style.textContent = css;
81
document.querySelector("head").appendChild(style);
82
}
83
amendCssOnce = 0;
84
}
85
86
/* The <span> object that holds the tooltip */
87
var tooltipObj = document.createElement("span");
88
tooltipObj.className = "tl-tooltip";
89
tooltipObj.style.display = "none";
90
document.getElementsByClassName("content")[0].appendChild(tooltipObj);
91
tooltipObj.onmouseenter = function(){
92
/* Hold the tooltip constant as long as the mouse is over the tooltip.
93
** In other words, do not let any of the timers changes the tooltip while
94
** the mouse is directly over the tooltip. This makes it easier for the
95
** user to move over top of the "copy-button" or the hyperlink to the
96
** /info page. */
97
stopCloseTimer();
98
stopDwellTimer();
99
tooltipInfo.ixHover = tooltipInfo.ixActive;
100
}
101
tooltipObj.onmouseleave = function(){
102
if (tooltipInfo.ixActive != -1) resumeCloseTimer();
103
};
104
105
/* State information for the tooltip popup and its timers */
106
window.tooltipInfo = {
107
dwellTimeout: 250, /* The tooltip dwell timeout. */
108
closeTimeout: 3000, /* The tooltip close timeout. */
109
hashDigits: 16, /* Limit of tooltip hashes ("hash-digits"). */
110
idTimer: 0, /* The tooltip dwell timer id */
111
idTimerClose: 0, /* The tooltip close timer id */
112
ixHover: -1, /* The mouse is over a thick riser arrow for
113
** tx.rowinfo[ixHover]. Or -2 when the mouse is
114
** over a graph node. Or -1 when the mouse is not
115
** over anything. */
116
ixActive: -1, /* The item shown in the tooltip is tx.rowinfo[ixActive].
117
** ixActive is -1 if the tooltip is not visible */
118
nodeHover: null, /* Graph node under mouse when ixHover==-2 */
119
idNodeActive: 0, /* Element ID of the graph node with the tooltip. */
120
posX: 0, posY: 0 /* The last mouse position. */
121
};
122
123
/* Functions used to control the tooltip popup and its timer */
124
function onKeyDown(event){ /* Hide the tooltip when ESC key pressed */
125
var key = event.which || event.keyCode;
126
if( key==27 ){
127
event.stopPropagation();
128
hideGraphTooltip();
129
}
130
}
131
function hideGraphTooltip(){ /* Hide the tooltip */
132
document.removeEventListener('keydown',onKeyDown,/* useCapture == */true);
133
stopCloseTimer();
134
tooltipObj.style.display = "none";
135
tooltipInfo.ixActive = -1;
136
tooltipInfo.idNodeActive = 0;
137
}
138
window.onpagehide = hideGraphTooltip;
139
function stopDwellTimer(){
140
if(tooltipInfo.idTimer!=0){
141
clearTimeout(tooltipInfo.idTimer);
142
tooltipInfo.idTimer = 0;
143
}
144
}
145
function resumeCloseTimer(){
146
/* This timer must be stopped explicitly to reset the elapsed timeout. */
147
if(tooltipInfo.idTimerClose==0 && tooltipInfo.closeTimeout>0) {
148
tooltipInfo.idTimerClose = setTimeout(function(){
149
tooltipInfo.idTimerClose = 0;
150
hideGraphTooltip();
151
},tooltipInfo.closeTimeout);
152
}
153
}
154
function stopCloseTimer(){
155
if(tooltipInfo.idTimerClose!=0){
156
clearTimeout(tooltipInfo.idTimerClose);
157
tooltipInfo.idTimerClose = 0;
158
}
159
}
160
161
/* Construct that graph corresponding to the timeline-data-N object that
162
** is passed in by the tx parameter */
163
function TimelineGraph(tx){
164
var topObj = document.getElementById("timelineTable"+tx.iTableId);
165
amendCss(tx.circleNodes, tx.showArrowheads);
166
tooltipInfo.dwellTimeout = tx.dwellTimeout
167
tooltipInfo.closeTimeout = tx.closeTimeout
168
tooltipInfo.hashDigits = tx.hashDigits
169
topObj.onclick = clickOnGraph
170
topObj.ondblclick = dblclickOnGraph
171
topObj.onmousemove = function(e) {
172
var ix = findTxIndex(e);
173
topObj.style.cursor = (ix<0) ? "" : "pointer"
174
mouseOverGraph(e,ix,null);
175
};
176
topObj.onmouseleave = function(e) {
177
/* Hide the tooltip if the mouse is outside the "timelineTableN" element,
178
** and outside the tooltip. */
179
if(e.relatedTarget && e.relatedTarget != tooltipObj){
180
tooltipInfo.ixHover = -1;
181
hideGraphTooltip();
182
stopDwellTimer();
183
stopCloseTimer();
184
}
185
};
186
function mouseOverNode(e){ /* Invoked by mousemove events over a graph node */
187
e.stopPropagation()
188
mouseOverGraph(e,-2,this)
189
}
190
/* Combined mousemove handler for graph nodes and rails. */
191
function mouseOverGraph(e,ix,node){
192
stopDwellTimer(); // Mouse movement: reset the dwell timer.
193
var ownTooltip = // Check if the hovered element already has the tooltip.
194
(ix>=0 && ix==tooltipInfo.ixActive) ||
195
(ix==-2 && tooltipInfo.idNodeActive==node.id);
196
if(ownTooltip) stopCloseTimer(); // ownTooltip: clear the close timer.
197
else resumeCloseTimer(); // !ownTooltip: resume the close timer.
198
tooltipInfo.ixHover = ix;
199
tooltipInfo.nodeHover = node;
200
tooltipInfo.posX = e.clientX;
201
tooltipInfo.posY = e.clientY;
202
if(ix!=-1 && !ownTooltip && tooltipInfo.dwellTimeout>0){ // Go dwell timer.
203
tooltipInfo.idTimer = setTimeout(function(){
204
tooltipInfo.idTimer = 0;
205
stopCloseTimer();
206
showGraphTooltip();
207
},tooltipInfo.dwellTimeout);
208
}
209
}
210
var canvasDiv;
211
var railPitch;
212
var mergeOffset;
213
var node, arrow, arrowSmall, line, mArrow, mLine, wArrow, wLine;
214
215
function initGraph(){
216
var parent = topObj.rows[0].cells[1];
217
parent.style.verticalAlign = "top";
218
canvasDiv = document.createElement("div");
219
canvasDiv.className = "tl-canvas";
220
canvasDiv.style.position = "absolute";
221
parent.appendChild(canvasDiv);
222
223
var elems = {};
224
var elemClasses = [
225
"rail", "mergeoffset", "node", "arrow u", "arrow u sm", "line",
226
"arrow merge r", "line merge", "arrow warp", "line warp",
227
"line cherrypick", "line dotted"
228
];
229
for( var i=0; i<elemClasses.length; i++ ){
230
var cls = elemClasses[i];
231
var elem = document.createElement("div");
232
elem.className = "tl-" + cls;
233
if( cls.indexOf("line")==0 ) elem.className += " v";
234
canvasDiv.appendChild(elem);
235
var k = cls.replace(/\s/g, "_");
236
var r = elem.getBoundingClientRect();
237
var w = Math.round(r.right - r.left);
238
var h = Math.round(r.bottom - r.top);
239
elems[k] = {w: w, h: h, cls: cls};
240
}
241
node = elems.node;
242
arrow = elems.arrow_u;
243
arrowSmall = elems.arrow_u_sm;
244
line = elems.line;
245
mArrow = elems.arrow_merge_r;
246
mLine = elems.line_merge;
247
cpLine = elems.line_cherrypick;
248
wArrow = elems.arrow_warp;
249
wLine = elems.line_warp;
250
dotLine = elems.line_dotted;
251
252
var minRailPitch = Math.ceil((node.w+line.w)/2 + mArrow.w + 1);
253
if( window.innerWidth<400 ){
254
railPitch = minRailPitch;
255
}else{
256
if( tx.iRailPitch>0 ){
257
railPitch = tx.iRailPitch;
258
}else{
259
railPitch = elems.rail.w;
260
railPitch -= Math.floor((tx.nrail-1)*(railPitch-minRailPitch)/21);
261
}
262
railPitch = Math.max(railPitch, minRailPitch);
263
}
264
265
if( tx.nomo ){
266
mergeOffset = 0;
267
}else{
268
mergeOffset = railPitch-minRailPitch-mLine.w;
269
mergeOffset = Math.min(mergeOffset, elems.mergeoffset.w);
270
mergeOffset = mergeOffset>0 ? mergeOffset + line.w/2 : 0;
271
}
272
273
var canvasWidth = (tx.nrail-1)*railPitch + node.w;
274
canvasDiv.style.width = canvasWidth + "px";
275
canvasDiv.style.position = "relative";
276
}
277
function drawBox(cls,color,x0,y0,x1,y1){
278
var n = document.createElement("div");
279
x0 = Math.floor(x0);
280
y0 = Math.floor(y0);
281
x1 = x1 || x1===0 ? Math.floor(x1) : x0;
282
y1 = y1 || y1===0 ? Math.floor(y1) : y0;
283
if( x0>x1 ){ var t=x0; x0=x1; x1=t; }
284
if( y0>y1 ){ var t=y0; y0=y1; y1=t; }
285
var w = x1-x0;
286
var h = y1-y0;
287
n.style.position = "absolute";
288
n.style.left = x0+"px";
289
n.style.top = y0+"px";
290
if( w ) n.style.width = w+"px";
291
if( h ) n.style.height = h+"px";
292
if( color ) n.style.backgroundColor = color;
293
n.className = "tl-"+cls;
294
canvasDiv.appendChild(n);
295
return n;
296
}
297
function absoluteY(obj){
298
var y = 0;
299
do{
300
y += obj.offsetTop;
301
}while( obj = obj.offsetParent );
302
return y;
303
}
304
function absoluteX(obj){
305
var x = 0;
306
do{
307
x += obj.offsetLeft;
308
}while( obj = obj.offsetParent );
309
return x;
310
}
311
function miLineY(p){
312
return p.y + node.h - mLine.w - 1;
313
}
314
function drawLine(elem,color,x0,y0,x1,y1){
315
var cls = elem.cls + " ";
316
if( x1===null ){
317
x1 = x0+elem.w;
318
cls += "v";
319
}else{
320
y1 = y0+elem.w;
321
cls += "h";
322
}
323
return drawBox(cls,color,x0,y0,x1,y1);
324
}
325
function drawUpArrow(from,to,color,id){
326
var y = to.y + node.h;
327
var arrowSpace = from.y - y + (!from.id || from.r!=to.r ? node.h/2 : 0);
328
var arw = arrowSpace < arrow.h*1.5 ? arrowSmall : arrow;
329
var x = to.x + (node.w-line.w)/2;
330
var y0 = from.y + node.h/2;
331
var y1 = Math.ceil(to.y + node.h + arw.h/2);
332
var n = drawLine(line,color,x,y0,null,y1);
333
addToolTip(n,id)
334
x = to.x + (node.w-arw.w)/2;
335
n = drawBox(arw.cls,null,x,y);
336
if(color) n.style.borderBottomColor = color;
337
addToolTip(n,id)
338
}
339
function drawDotted(from,to,color,id){
340
var x = to.x + (node.w-line.w)/2;
341
var y0 = from.y + node.h/2;
342
var y1 = Math.ceil(to.y + node.h);
343
var n = drawLine(dotLine,null,x,y0,null,y1)
344
if( color ) n.style.borderColor = color
345
addToolTip(n,id)
346
}
347
function addToolTip(n,id){
348
if( id ) n.setAttribute("data-ix",id-tx.iTopRow)
349
}
350
/* Draw thin horizontal or vertical lines representing merges */
351
function drawMergeLine(x0,y0,x1,y1){
352
drawLine(mLine,null,x0,y0,x1,y1);
353
}
354
function drawCherrypickLine(x0,y0,x1,y1){
355
drawLine(cpLine,null,x0,y0,x1,y1);
356
}
357
/* Draw an arrow representing an in-bound merge from the "rail"-th rail
358
** over to the node of "p". Make it a checkpoint merge is "isCP" is true */
359
function drawMergeArrow(p,rail,isCP){
360
var x0 = rail*railPitch + node.w/2;
361
if( rail in mergeLines ){
362
x0 += mergeLines[rail];
363
if( p.r<rail ) x0 += mLine.w;
364
}else{
365
x0 += (p.r<rail ? -1 : 1)*line.w/2;
366
}
367
var x1 = mArrow.w ? mArrow.w/2 : -node.w/2;
368
x1 = p.x + (p.r<rail ? node.w + Math.ceil(x1) : -x1);
369
var y = miLineY(p);
370
var x = p.x + (p.r<rail ? node.w : -mArrow.w);
371
var cls;
372
if( isCP ){
373
drawCherrypickLine(x0,y,x1,null);
374
cls = "arrow cherrypick " + (p.r<rail ? "l" : "r");
375
}else{
376
drawMergeLine(x0,y,x1,null);
377
cls = "arrow merge " + (p.r<rail ? "l" : "r");
378
}
379
drawBox(cls,null,x,y+(mLine.w-mArrow.h)/2);
380
}
381
function drawNode(p, btm){
382
if( p.bg ){
383
var e = document.getElementById("mc"+p.id);
384
if(e) e.style.backgroundColor = p.bg;
385
e = document.getElementById("md"+p.id);
386
if(e) e.style.backgroundColor = p.bg;
387
}
388
if( p.r<0 ) return;
389
if( p.u>0 ) drawUpArrow(p,tx.rowinfo[p.u-tx.iTopRow],p.fg,p.id);
390
if( p.sb>0 ) drawDotted(p,tx.rowinfo[p.sb-tx.iTopRow],p.fg,p.id);
391
var cls = node.cls;
392
if( p.hasOwnProperty('mi') && p.mi.length ) cls += " merge";
393
if( p.f&2 ) cls += " closed-leaf";
394
else if( p.f&1 ) cls += " leaf";
395
var n = drawBox(cls,p.bg,p.x,p.y);
396
n.id = "tln"+p.id;
397
n.onclick = clickOnNode;
398
n.ondblclick = dblclickOnNode;
399
n.onmousemove = mouseOverNode;
400
n.style.zIndex = 10;
401
if( p.f&2 ){
402
var pt1 = 0;
403
var pt2 = 100;
404
if( tx.circleNodes ){
405
pt1 = 14;
406
pt2 = 86;
407
}
408
n.innerHTML = "<svg width='100%' height='100%'viewbox='0 0 100 100'>"
409
+ `<path d='M ${pt1},${pt1} L ${pt2},${pt2} M ${pt1},${pt2} L ${pt2},${pt1}'`
410
+ " stroke='currentcolor' stroke-width='13'/>"
411
+ "</svg>";
412
}
413
if( !tx.omitDescenders ){
414
if( p.u==0 ){
415
if( p.hasOwnProperty('mo') && p.r==p.mo ){
416
var ix = p.hasOwnProperty('cu') ? p.cu : p.mu;
417
var top = tx.rowinfo[ix-tx.iTopRow]
418
drawUpArrow(p,{x: p.x, y: top.y-node.h}, p.fg, p.id);
419
}else if( p.y>100 ){
420
drawUpArrow(p,{x: p.x, y: p.y-50}, p.fg, p.id);
421
}else{
422
drawUpArrow(p,{x: p.x, y: 0},p.fg, p.id);
423
}
424
}
425
if( p.hasOwnProperty('d') ){
426
if( p.y + 150 >= btm ){
427
drawUpArrow({x: p.x, y: btm - node.h/2},p,p.fg,p.id);
428
}else{
429
drawUpArrow({x: p.x, y: p.y+50},p,p.fg,p.id);
430
drawDotted({x: p.x, y: p.y+63},{x: p.x, y: p.y+50-node.h/2},p.fg,p.id);
431
}
432
}
433
}
434
if( p.hasOwnProperty('mo') ){
435
var x0 = p.x + node.w/2;
436
var x1 = p.mo*railPitch + node.w/2;
437
var u = tx.rowinfo[p.mu-tx.iTopRow];
438
var mtop = u;
439
if( p.hasOwnProperty('cu') ){
440
mtop = tx.rowinfo[p.cu-tx.iTopRow];
441
}
442
var y1 = miLineY(u);
443
if( p.u<=0 || p.mo!=p.r ){
444
if( p.u==0 && p.mo==p.r ){
445
mergeLines[p.mo] = mtop.r<p.r ? -mergeOffset-mLine.w : mergeOffset;
446
}else{
447
mergeLines[p.mo] = -mLine.w/2;
448
}
449
x1 += mergeLines[p.mo]
450
var y0 = p.y+2;
451
var isCP = p.hasOwnProperty('cu');
452
if( p.mu==p.id ){
453
/* Special case: The merge riser already exists. Only draw the
454
/* horizontal line or arrow going from the node out to the riser. */
455
var dx = x1<x0 ? mArrow.w : -mArrow.w;
456
if( isCP ){
457
drawCherrypickLine(x0,y0,x1+dx,null);
458
cls = "arrow cherrypick " + (x1<x0 ? "l" : "r");
459
}else{
460
drawMergeLine(x0,y0,x1+dx,null);
461
cls = "arrow merge " + (x1<x0 ? "l" : "r");
462
}
463
if( !isCP || p.mu==p.cu ){
464
dx = x1<x0 ? mLine.w : -(mArrow.w + mLine.w/2);
465
drawBox(cls,null,x1+dx,y0+(mLine.w-mArrow.h)/2);
466
}
467
y1 = y0;
468
}else{
469
drawMergeLine(x0,y0,x1+(x0<x1 ? mLine.w : 0),null);
470
drawMergeLine(x1,y0+mLine.w,null,y1);
471
}
472
if( isCP && p.cu!=p.id ){
473
var u2 = tx.rowinfo[p.cu-tx.iTopRow];
474
var y2 = miLineY(u2);
475
drawCherrypickLine(x1,y1,null,y2);
476
}
477
}else if( mergeOffset ){
478
mergeLines[p.mo] = mtop.r<p.r ? -mergeOffset-mLine.w : mergeOffset;
479
x1 += mergeLines[p.mo];
480
if( p.mu<p.id ){
481
drawMergeLine(x1,p.y+node.h/2,null,y1);
482
}
483
if( p.hasOwnProperty('cu') ){
484
var u2 = tx.rowinfo[p.cu-tx.iTopRow];
485
var y2 = miLineY(u2);
486
drawCherrypickLine(x1,y1,null,y2);
487
}
488
}else{
489
delete mergeLines[p.mo];
490
}
491
}
492
if( p.hasOwnProperty('au') ){
493
for( var i=0; i<p.au.length; i+=2 ){
494
var rail = p.au[i];
495
var x0 = p.x + node.w/2;
496
var x1 = rail*railPitch + (node.w-line.w)/2;
497
if( x0<x1 ){
498
x0 = Math.ceil(x0);
499
x1 += line.w;
500
}
501
var y0 = p.y + (node.h-line.w)/2;
502
var u = tx.rowinfo[p.au[i+1]-tx.iTopRow];
503
if( u.id<p.id ){
504
// normal thick up-arrow
505
drawLine(line,u.fg,x0,y0,x1,null);
506
drawUpArrow(p,u,u.fg,u.id);
507
}else{
508
// timewarp: The child node occurs before the parent
509
var y1 = u.y + (node.h-line.w)/2;
510
var n = drawLine(wLine,u.fg,x0,y0,x1,null);
511
addToolTip(n,u.id)
512
n = drawLine(wLine,u.fg,x1-line.w,y0,null,y1+line.w);
513
addToolTip(n,u.id)
514
n = drawLine(wLine,u.fg,x1,y1,u.x-wArrow.w/2,null);
515
addToolTip(n,u.id)
516
var x = u.x-wArrow.w;
517
var y = u.y+(node.h-wArrow.h)/2;
518
n = drawBox(wArrow.cls,null,x,y);
519
addToolTip(n,u.id)
520
if( u.fg ) n.style.borderLeftColor = u.fg;
521
}
522
}
523
}
524
if( p.hasOwnProperty('mi') ){
525
for( var i=0; i<p.mi.length; i++ ){
526
var rail = p.mi[i];
527
if( rail<0 ){
528
rail = -1-rail;
529
mergeLines[rail] = -mLine.w/2;
530
var x = rail*railPitch + (node.w-mLine.w)/2;
531
var y = miLineY(p);
532
drawMergeLine(x,y,null,mergeBtm[rail]);
533
mergeBtm[rail] = y;
534
}
535
drawMergeArrow(p,rail,0);
536
}
537
}
538
if( p.hasOwnProperty('ci') ){
539
for( var i=0; i<p.ci.length; i++ ){
540
var rail = p.ci[i];
541
if( rail<0 ){
542
rail = -rail;
543
mergeLines[rail] = -mLine.w/2;
544
var x = rail*railPitch + (node.w-mLine.w)/2;
545
var y = miLineY(p);
546
drawCherrypickLine(x,y,null,mergeBtm[rail]);
547
mergeBtm[rail] = y;
548
}
549
drawMergeArrow(p,rail,1);
550
}
551
}
552
}
553
var mergeLines;
554
var mergeBtm = new Array;
555
function renderGraph(){
556
mergeLines = {};
557
canvasDiv.innerHTML = "";
558
var canvasY = absoluteY(canvasDiv);
559
for(var i=0; i<tx.rowinfo.length; i++ ){
560
var e = document.getElementById("m"+tx.rowinfo[i].id);
561
tx.rowinfo[i].y = absoluteY(e) - canvasY;
562
tx.rowinfo[i].x = tx.rowinfo[i].r*railPitch;
563
}
564
var tlBtm = document.getElementById(tx.bottomRowId);
565
if( tlBtm.offsetHeight<node.h ){
566
tlBtm.style.height = node.h + "px";
567
}
568
var btm = absoluteY(tlBtm) - canvasY + tlBtm.offsetHeight;
569
for( var i=0; i<tx.nrail; i++) mergeBtm[i] = btm;
570
for( var i=tx.rowinfo.length-1; i>=0; i-- ){
571
drawNode(tx.rowinfo[i], btm);
572
}
573
}
574
var selRow;
575
function clickOnNode(e){
576
hideGraphTooltip()
577
var p = tx.rowinfo[parseInt(this.id.match(/\d+$/)[0], 10)-tx.iTopRow];
578
if( !selRow ){
579
selRow = p;
580
this.className += " sel";
581
canvasDiv.className += " sel";
582
}else if( selRow==p ){
583
selRow = null;
584
this.className = this.className.replace(" sel", "");
585
canvasDiv.className = canvasDiv.className.replace(" sel", "");
586
}else{
587
if( tx.fileDiff ){
588
location.href=tx.baseUrl + "/fdiff?v1="+selRow.h+"&v2="+p.h;
589
}else{
590
var href = tx.baseUrl + "/vdiff?from="+selRow.h+"&to="+p.h;
591
let params = (new URL(document.location)).searchParams;
592
if(params && typeof params === "object"){
593
/* When called from /timeline page, If chng=str was specified in the
594
** QueryString, specify glob=str on the /vdiff page */
595
let glob = params.get("chng");
596
if( !glob ){
597
/* When called from /vdiff page, keep the glob= QueryString if
598
** present. */
599
glob = params.get("glob");
600
}
601
if( glob ){
602
href += "&glob=" + glob;
603
}
604
}
605
location.href = href;
606
}
607
}
608
e.stopPropagation()
609
}
610
function dblclickOnNode(e){
611
var p = tx.rowinfo[parseInt(this.id.match(/\d+$/)[0], 10)-tx.iTopRow];
612
window.location.href = tx.baseUrl+"/info/"+p.h
613
e.stopPropagation()
614
}
615
function findTxIndex(e){
616
if( !tx.rowinfo ) return -1;
617
/* Look at all the graph elements. If any graph elements that is near
618
** the click-point "e" and has a "data-ix" attribute, then return
619
** the value of that attribute. Otherwise return -1 */
620
var x = e.clientX + window.pageXOffset - absoluteX(canvasDiv);
621
var y = e.clientY + window.pageYOffset - absoluteY(canvasDiv);
622
var aNode = canvasDiv.childNodes
623
var nNode = aNode.length;
624
var i;
625
for(i=0;i<nNode;i++){
626
var n = aNode[i]
627
if( !n.hasAttribute("data-ix") ) continue;
628
if( x<n.offsetLeft-5 ) continue;
629
if( x>n.offsetLeft+n.offsetWidth+5 ) continue;
630
if( y<n.offsetTop-5 ) continue;
631
if( y>n.offsetTop+n.offsetHeight ) continue;
632
return n.getAttribute("data-ix")
633
}
634
return -1
635
}
636
/* Compute the hyperlink for the branch graph for tx.rowinfo[ix] */
637
function branchHyperlink(ix){
638
var br = tx.rowinfo[ix].br
639
var dest = tx.baseUrl + "/timeline?r=" + encodeURIComponent(br)
640
dest += tx.fileDiff ? "&m&cf=" : "&m&c="
641
dest += encodeURIComponent(tx.rowinfo[ix].h)
642
return dest
643
}
644
function clickOnGraph(e){
645
stopCloseTimer();
646
stopDwellTimer();
647
tooltipInfo.ixHover = findTxIndex(e);
648
tooltipInfo.posX = e.clientX;
649
tooltipInfo.posY = e.clientY;
650
showGraphTooltip();
651
}
652
function showGraphTooltip(){
653
var html = null
654
var ix = -1
655
if( tooltipInfo.ixHover==-2 ){
656
ix = parseInt(tooltipInfo.nodeHover.id.match(/\d+$/)[0],10)-tx.iTopRow
657
var h = tx.rowinfo[ix].h
658
var dest = tx.baseUrl + "/info/" + h
659
h = h.slice(0,tooltipInfo.hashDigits); // Assume single-byte characters.
660
if( tx.fileDiff ){
661
html = "artifact <a id=\"tooltip-link\" href=\""+dest+"\">"+h+"</a>"
662
}else{
663
html = "check-in <a id=\"tooltip-link\" href=\""+dest+"\">"+h+"</a>"
664
}
665
tooltipInfo.ixActive = -2;
666
tooltipInfo.idNodeActive = tooltipInfo.nodeHover.id;
667
}else if( tooltipInfo.ixHover>=0 ){
668
ix = tooltipInfo.ixHover
669
var br = tx.rowinfo[ix].br
670
var dest = branchHyperlink(ix)
671
var hbr = br.replace(/&/g, "&amp;")
672
.replace(/</g, "&lt;")
673
.replace(/>/g, "&gt;")
674
.replace(/"/g, "&quot;")
675
.replace(/'/g, "&#039;");
676
html = "branch <a id=\"tooltip-link\" href=\""+dest+"\">"+hbr+"</a>"
677
tooltipInfo.ixActive = ix;
678
tooltipInfo.idNodeActive = 0;
679
}
680
if( html ){
681
/* Setup while hidden, to ensure proper dimensions. */
682
var s = getComputedStyle(document.body)
683
if( tx.rowinfo[ix].bg.length ){
684
tooltipObj.style.backgroundColor = tx.rowinfo[ix].bg
685
}else{
686
tooltipObj.style.backgroundColor = s.getPropertyValue('background-color')
687
}
688
tooltipObj.style.borderColor =
689
tooltipObj.style.color = s.getPropertyValue('color')
690
tooltipObj.style.visibility = "hidden"
691
tooltipObj.innerHTML = html
692
tooltipObj.insertBefore(makeCopyButton("tooltip-link",0,0),
693
tooltipObj.childNodes[1]);
694
tooltipObj.style.display = "inline"
695
tooltipObj.style.position = "absolute"
696
var x = tooltipInfo.posX + 4 + window.pageXOffset
697
- absoluteX(tooltipObj.offsetParent)
698
tooltipObj.style.left = x+"px"
699
var y = tooltipInfo.posY + window.pageYOffset
700
- tooltipObj.clientHeight - 4
701
- absoluteY(tooltipObj.offsetParent)
702
tooltipObj.style.top = y+"px"
703
tooltipObj.style.visibility = "visible"
704
document.addEventListener('keydown',onKeyDown,/* useCapture == */true);
705
}else{
706
hideGraphTooltip()
707
}
708
}
709
function dblclickOnGraph(e){
710
var ix = findTxIndex(e);
711
hideGraphTooltip()
712
if( ix>=0 ){
713
var dest = branchHyperlink(ix)
714
window.location.href = dest
715
}
716
}
717
function changeDisplay(selector,value){
718
var x = document.getElementsByClassName(selector);
719
var n = x.length;
720
for(var i=0; i<n; i++) {x[i].style.display = value;}
721
}
722
function changeDisplayById(id,value){
723
var x = document.getElementById(id);
724
if(x) x.style.display=value;
725
}
726
function toggleDetail(evt){
727
/* Ignore clicks to hyperlinks and other "click-responsive" HTML elements.
728
** This click-handler is set for <SPAN> elements with the CSS class names
729
** "timelineEllipsis" and "timelineCompactComment", which are part of the
730
** "Compact" and "Simple" views. */
731
var xClickyHTML = /^(?:A|AREA|BUTTON|INPUT|LABEL|SELECT|TEXTAREA|DETAILS)$/;
732
if( xClickyHTML.test(evt.target.tagName) ) return;
733
var id = parseInt(this.getAttribute('data-id'))
734
var x = document.getElementById("detail-"+id);
735
if( x.style.display=="inline" ){
736
x.style.display="none";
737
document.getElementById("ellipsis-"+id).textContent = "...";
738
changeDisplayById("links-"+id,"none");
739
}else{
740
x.style.display="inline";
741
document.getElementById("ellipsis-"+id).textContent = "←";
742
changeDisplayById("links-"+id,"inline");
743
}
744
checkHeight();
745
}
746
function scrollToSelected(){
747
var x = document.getElementsByClassName('timelineSelected');
748
if(x[0]){
749
var h = window.innerHeight;
750
var y = absoluteY(x[0]) - h/2;
751
if( y>0 ) window.scrollTo(0, y);
752
}
753
}
754
if( tx.rowinfo ){
755
var lastRow =
756
document.getElementById("m"+tx.rowinfo[tx.rowinfo.length-1].id);
757
var lastY = 0;
758
function checkHeight(){
759
var h = absoluteY(lastRow);
760
if( h!=lastY ){
761
renderGraph();
762
lastY = h;
763
}
764
setTimeout(checkHeight, 1000);
765
}
766
initGraph();
767
checkHeight();
768
}else{
769
function checkHeight(){}
770
}
771
if( tx.scrollToSelect ){
772
scrollToSelected();
773
}
774
775
/* Set the onclick= attributes for elements of the "Compact" and
776
** "Simple" views so that clicking turns the details on and off.
777
*/
778
var lx = topObj.getElementsByClassName('timelineEllipsis');
779
var i;
780
for(i=0; i<lx.length; i++){
781
if( lx[i].hasAttribute('data-id') ){
782
lx[i].addEventListener('click',toggleDetail);
783
}
784
}
785
lx = topObj.getElementsByClassName('timelineCompactComment');
786
for(i=0; i<lx.length; i++){
787
if( lx[i].hasAttribute('data-id') ){
788
lx[i].addEventListener('click',toggleDetail);
789
}
790
}
791
if( window.innerWidth<400 ){
792
/* On narrow displays, shift the date from the first column to the
793
** third column, to make the first column narrower */
794
lx = topObj.getElementsByClassName('timelineDateRow');
795
for(i=0; i<lx.length; i++){
796
var rx = lx[i];
797
if( rx.getAttribute('data-reordered') ) break;
798
rx.setAttribute('data-reordered',1);
799
rx.appendChild(rx.firstChild);
800
rx.insertBefore(rx.childNodes[1],rx.firstChild);
801
}
802
/* Do not show the HH:MM timestamps on very narrow displays
803
** as they take up too much horizontal space. */
804
lx = topObj.getElementsByClassName('timelineHistLink');
805
for(i=0; i<lx.length; i++){
806
var rx = lx[i];
807
rx.style.display="none";
808
}
809
}
810
}
811
812
/* Look for all timeline-data-NN objects. Load each one and draw
813
** a graph for each one.
814
*/
815
(function(){
816
var i;
817
for(i=0; 1; i++){
818
var dataObj = document.getElementById("timeline-data-"+i);
819
if(!dataObj) break;
820
var txJson = dataObj.textContent || dataObj.innerText;
821
var tx = JSON.parse(txJson);
822
TimelineGraph(tx);
823
}
824
}());
825
826
/*
827
** Timeline keyboard navigation shortcuts:
828
**
829
** ### NOTE: The keyboard shortcuts are listed in the /timeline help screen. ###
830
**
831
** When navigating to a page with a timeline display, such as /timeline, /info,
832
** or /finfo, keyboard navigation mode needs to be "activated" first, i.e. if no
833
** timeline entry is focused yet, pressing any of the listed keys (except ESC)
834
** sets the visual focus indicator to the highlighted or current (check-out)
835
** entry if available, or to the topmost entry otherwise. A session cookie[0] is
836
** used to direct pages loaded in the future to enable keyboard navigation mode
837
** and automatically set the focus indicator to the highlighted, current, or
838
** topmost entry. Pressing N and M on the /timeline page while the topmost or
839
** bottommost entry is focused loads the next or previous page if available,
840
** similar to the [↑ More] and [↓ More] links. Pressing ESC disables keyboard
841
** navigation, i.e. removes the focus indicator and deletes the session cookie.
842
** When navigating backwards or forwards in browser history, the focused entry
843
** is restored using a hidden[1] input field.
844
**
845
** [0]: The lifetime and values of cookies can be tracked on the /cookies page.
846
** A session cookie is preferred over other storage APIs because Fossil already
847
** requires cookies to be enabled for reasonable functionality, and it's more
848
** likely that other storage APIs are blocked by users for privacy reasons, for
849
** example.
850
** [1]: This feature only works with a normal (text) input field hidden by CSS
851
** styles, instead of a true hidden (by type) input field, but according to MDN,
852
** screen readers should ignore it even without an aria-hidden="true" attribute
853
** (which is even discouraged for hidden by CSS elements). Also, this feature
854
** breaks if disabled[=true] or tabindex="-1" attributes are added to the input
855
** field, or (in FF) if page unload handlers are present.
856
**
857
** Ideas and TODOs:
858
**
859
** o kTMLN: ensure the correct page is opened when used from /finfo (it seems
860
** the tooltip also gets this "wrong", but maybe that's acceptable, because
861
** in order to be able to construct /file URLs, the information provided by
862
** the timeline-data-N blocks would have to be extended).
863
** o kFRST, kLAST: check if the previous/next page should be opened if focus is
864
** already at the top/bottom. UPDATE: the current behavior seems to be "more
865
** predictable", i.e. these shortcuts reliably focus the top/bottom item.
866
** o Shortcut(s) to (re)load /timeline with different View Style or Entry Limit
867
** by appending query parameters `&ss={ViewStyle}&n={EntryLimit±N}&udc=1' to
868
** the URL; alternatively set keyboard focus to the "View Style" <select> or
869
** to the "Max:" <input> field.
870
** o Auto-expand the hidden details (hash, user, tags) for focused entries in
871
** Compact View (by inheritance via CSS class `.timelineFocused').
872
** o Shortcut(s) to (re)load /ckout (with or without `exbase' query parameter).
873
*/
874
(function(){
875
window.addEventListener('load',function(){
876
// "Primary" (1) and "secondary" (2) selections swapped compared to CSS classes:
877
// (1) .timelineSecondary →
878
// /vdiff?to=, /timeline?sel2=
879
// (2) .timelineSelected:not(.timelineSecondary) →
880
// /vdiff?from=, /timeline?c=, /timeline?sel1=
881
function focusDefaultId(){
882
var tn = document.querySelector('.timelineSecondary .tl-nodemark')
883
|| document.querySelector(
884
'.timelineSelected:not(.timelineSecondary) .tl-nodemark')
885
|| document.querySelector('.timelineCurrent .tl-nodemark');
886
return tn ? tn.id : 'm1';
887
}
888
function focusSelectedId(){
889
var tn = document.querySelector('.timelineSecondary .tl-nodemark');
890
return tn ? tn.id : null;
891
}
892
function focus2ndSelectedId(){
893
var tn = document.querySelector(
894
'.timelineSelected:not(.timelineSecondary) .tl-nodemark');
895
return tn ? tn.id : null;
896
}
897
function focusCurrentId(){
898
var tn = document.querySelector('.timelineCurrent .tl-nodemark');
899
return tn ? tn.id : null;
900
}
901
function focusTickedId(){
902
var nd = document.querySelector('.tl-node.sel');
903
return nd ? 'm' + nd.id.slice(3) : null;
904
}
905
function focusFirstId(id){
906
return 'm1';
907
}
908
function focusLastId(id){
909
var el = document.getElementsByClassName('tl-nodemark');
910
var tn = el ? el[el.length-1] : null;
911
return tn ? tn.id : id;
912
}
913
function focusNextId(id,dx){
914
if( dx<-1 ) return focusFirstId(id);
915
if( dx>+1 ) return focusLastId(id);
916
var m = /^m(\d+)$/.exec(id);
917
return m!==null ? 'm' + (parseInt(m[1]) + dx) : null;
918
}
919
// NOTE: This code relies on `getElementsByClassName()' returning elements
920
// in document order; however, attempts to use binary searching (using the
921
// signed distance to the vertical center) or 2-pass searching (to get the
922
// 1000-block first) didn't yield any significant speedups -- probably the
923
// calls to `getBoundingClientRect()' are cached because `initGraph()' has
924
// already done so, or because they depend on their previous siblings, and
925
// so `e(N).getBoundingClientRect()' ≈ `e(0..N).getBoundingClientRect()'?
926
function focusViewportCenterId(){
927
var
928
fe = null,
929
fd = 0,
930
wt = 0,
931
wb = wt + window.innerHeight,
932
wc = wt + window.innerHeight/2,
933
el = document.getElementsByClassName('timelineGraph');
934
for( var i=0; i<el.length; i++ ){
935
var
936
rc = el[i].getBoundingClientRect(),
937
et = rc.top,
938
eb = rc.bottom;
939
if( (et>=wt && et<=wb) || (eb>=wt && eb<=wb) ){
940
var ed = Math.min(Math.abs(et-wc),Math.abs(eb-wc));
941
if( !fe || ed<fd ){
942
fe = el[i];
943
fd = ed;
944
}
945
else break;
946
}
947
}
948
return fe ? fe.querySelector('.tl-nodemark').id : null;
949
}
950
function timelineGetDataBlock(i){
951
var tb = document.getElementById('timeline-data-' + i);
952
return tb ? JSON.parse(tb.textContent || tb.innerText) : null;
953
}
954
function timelineGetRowInfo(id){
955
var ti;
956
for(var i=0; ti=timelineGetDataBlock(i); i++){
957
// NOTE: `ti.rowinfo' only available if data block contains check-ins.
958
for( var k=0; ti.rowinfo && k<ti.rowinfo.length; k++ ){
959
if( id=='m' + ti.rowinfo[k].id ) return {
960
'baseurl': ti.baseUrl,
961
'filehash': ti.fileDiff,
962
'hashdigits': ti.hashDigits,
963
'hash': ti.rowinfo[k].h,
964
'branch': ti.rowinfo[k].br
965
};
966
}
967
}
968
return null;
969
}
970
function fossilScrollIntoView(e){
971
var y = 0;
972
do{
973
y += e.offsetTop;
974
}while( e = e.offsetParent );
975
window.scrollTo(0,y-window.innerHeight/2);
976
}
977
function focusVisualize(id,scroll){
978
var td = document.querySelector('.timelineFocused');
979
if( td ) td.classList.remove('timelineFocused');
980
if( !id ) return true;
981
var tn = document.getElementById(id);
982
if( tn ){
983
td = tn.parentElement.nextElementSibling;
984
if( td ) {
985
td.classList.add('timelineFocused');
986
if( scroll ) fossilScrollIntoView(td);
987
return true;
988
}
989
}
990
return false;
991
}
992
function focusCacheInit(){
993
var e = document.getElementById('timeline-kbfocus');
994
if( !e ){
995
e = document.createElement('input');
996
e.type = 'text';
997
e.style.display = 'none';
998
e.style.visibility = 'hidden';
999
e.id = 'timeline-kbfocus';
1000
document.body.appendChild(e);
1001
}
1002
}
1003
function focusCacheGet(){
1004
var e = document.getElementById('timeline-kbfocus');
1005
return e ? e.value : null;
1006
}
1007
function focusCacheSet(v){
1008
var e = document.getElementById('timeline-kbfocus');
1009
if( e ) e.value = v;
1010
}
1011
function focusCookieInit(){
1012
document.cookie = 'fossil_timeline_kbnav=1;path=/';
1013
}
1014
function focusCookieClear(){
1015
document.cookie =
1016
'fossil_timeline_kbnav=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/';
1017
}
1018
function focusCookieQuery(){
1019
return document.cookie.match(/fossil_timeline_kbnav=1/);
1020
}
1021
focusCacheInit();
1022
document.addEventListener('keydown',function(evt){
1023
if( evt.target.tagName=='INPUT' || evt.target.tagName=='SELECT' ) return;
1024
var
1025
mSHIFT = 1<<13,
1026
kFRST = mSHIFT | 78 /* SHIFT+N */,
1027
kNEXT = 78 /* N */,
1028
kPREV = 77 /* M */,
1029
kLAST = mSHIFT | 77 /* SHIFT+M */,
1030
kCYCL = 74 /* J */,
1031
kCNTR = 188 /* , */,
1032
kSCRL = mSHIFT | 74 /* SHIFT+J */,
1033
kTICK = 72 /* H */,
1034
kUNTK = mSHIFT | 72 /* SHIFT+H */,
1035
kCPYH = 66 /* B */,
1036
kCPYB = mSHIFT | 66 /* SHIFT+B */,
1037
kTMLN = 76 /* L */,
1038
kTMLB = mSHIFT | 76 /* SHIFT+L */,
1039
kVIEW = 75 /* K */,
1040
kVDEF = 71 /* G */,
1041
kVCUR = mSHIFT | 71 /* SHIFT+G */,
1042
kDONE = 27 /* ESC */,
1043
mod = evt.altKey<<15|evt.ctrlKey<<14|evt.shiftKey<<13|evt.metaKey<<12,
1044
key = ( evt.which || evt.keyCode ) | mod;
1045
var dx = 0;
1046
switch( key ){
1047
case kFRST: dx = -2; break;
1048
case kNEXT: dx = -1; break;
1049
case kPREV: dx = +1; break;
1050
case kLAST: dx = +2; break;
1051
case kCYCL:
1052
case kCNTR:
1053
case kSCRL:
1054
case kTICK:
1055
case kUNTK:
1056
case kCPYH:
1057
case kCPYB:
1058
case kTMLN:
1059
case kTMLB:
1060
case kVIEW:
1061
case kVDEF:
1062
case kVCUR:
1063
case kDONE: break;
1064
default: return;
1065
}
1066
evt.preventDefault();
1067
evt.stopPropagation();
1068
if( key==kCNTR ){
1069
var cid = focusViewportCenterId();
1070
if( cid ){
1071
focusCacheSet(cid);
1072
focusVisualize(cid,false);
1073
focusCookieInit();
1074
}
1075
return;
1076
}
1077
else if( key==kSCRL ){
1078
var td = document.querySelector('.timelineFocused');
1079
if( td ) fossilScrollIntoView(td);
1080
return;
1081
}
1082
else if( key==kUNTK ){
1083
var tid = focusTickedId();
1084
if( tid ){
1085
var gn = document.getElementById('tln'+tid.slice(1));
1086
if( gn ) gn.click();
1087
}
1088
return;
1089
}
1090
else if( key==kCPYH || key==kCPYB ){
1091
var fid = focusCacheGet();
1092
if( fid ){
1093
var ri = timelineGetRowInfo(fid);
1094
if( ri ){
1095
copyTextToClipboard(
1096
key==kCPYH ? ri.hash.slice(0,ri.hashdigits) : ri.branch);
1097
}
1098
}
1099
return;
1100
}
1101
else if( key==kVDEF || key==kVCUR ){
1102
var ri = timelineGetRowInfo('m1');
1103
if( ri ){
1104
var page;
1105
switch( key ){
1106
case kVDEF:
1107
page = '/timeline';
1108
break;
1109
case kVCUR:
1110
page = '/timeline?c=current';
1111
break;
1112
}
1113
var href = ri.baseurl + page;
1114
if( href!=location.href.slice(-href.length) ) location.href = href;
1115
}
1116
return;
1117
}
1118
else if( key==kDONE ){
1119
focusCacheSet(null);
1120
focusVisualize(null,false);
1121
focusCookieClear();
1122
return;
1123
}
1124
focusCookieInit();
1125
var id = focusCacheGet();
1126
if( id && dx==0 ){
1127
if( key==kCYCL ){
1128
function uniqIds(){
1129
var a = Array.prototype.slice.call(arguments,0), k=[], v=[];
1130
for( var i=0; i<a.length; i++ ){
1131
if( a[i] && !(a[i] in k) ){
1132
k[a[i]] = 1;
1133
v.push(a[i]);
1134
}
1135
}
1136
return v;
1137
}
1138
var ids = uniqIds(
1139
focusTickedId(),
1140
focusSelectedId(),
1141
focus2ndSelectedId(),
1142
focusCurrentId()
1143
);
1144
if( ids.length>1 ){
1145
var m = 0;
1146
for( var i=0; i<ids.length; i++ ){
1147
if( id==ids[i] ){
1148
m = i<ids.length-1 ? i+1 : 0;
1149
break;
1150
}
1151
}
1152
id = ids[m];
1153
}
1154
else if( ids.length==1 ) id = ids[0];
1155
}
1156
else if( key==kTICK ){
1157
var gn = document.getElementById('tln'+id.slice(1));
1158
if( gn ) gn.click();
1159
}
1160
else/* if( key==kTMLN || key==kTMLB || key==kVIEW )*/{
1161
var ri = timelineGetRowInfo(id);
1162
if( ri ){
1163
var hh = encodeURIComponent(ri.hash.slice(0,ri.hashdigits));
1164
var br = encodeURIComponent(ri.branch);
1165
var page;
1166
switch( key ){
1167
case kTMLN:
1168
page = '/timeline' + ( ri.filehash ? '?m&cf=' : '?m&c=' ) + hh;
1169
break;
1170
case kTMLB:
1171
page = '/timeline?r=' + br +
1172
( ri.filehash ? '&m&cf=' : '&m&c=' ) + hh;
1173
break;
1174
case kVIEW:
1175
page = '/info/' + hh;
1176
break;
1177
}
1178
var href = ri.baseurl + page;
1179
if( href!=location.href.slice(-href.length) ){
1180
location.href = href;
1181
return;
1182
}
1183
}
1184
}
1185
}
1186
else if ( id && dx!=0 ){
1187
id = focusNextId(id,dx);
1188
if( id && !document.getElementById(id) ){
1189
var btn =
1190
document.querySelector('.tl-button-' + ( dx>0 ? 'prev' : 'next' ));
1191
if( btn ) btn.click();
1192
return;
1193
}
1194
}
1195
else if ( !id ) id = focusDefaultId();
1196
focusCacheSet(id);
1197
focusVisualize(id,true);
1198
}/*,true*/);
1199
// NOTE: To be able to override the default CTRL+CLICK behavior (to "select"
1200
// the elements, indicated by an outline) for normal (non-input) elements in
1201
// FF it's necessary to listen to `mousedown' instead of `click' events.
1202
window.addEventListener('mousedown',function(evt){
1203
var
1204
bMAIN = 0,
1205
mCTRL = 1<<14,
1206
mod = evt.altKey<<15|evt.ctrlKey<<14|evt.shiftKey<<13|evt.metaKey<<12,
1207
xClickyHTML = /^(?:A|AREA|BUTTON|INPUT|LABEL|SELECT|TEXTAREA|DETAILS)$/;
1208
if( xClickyHTML.test(evt.target.tagName) ||
1209
evt.target.className=='timelineHash' ||
1210
evt.button!=bMAIN || mod!=mCTRL ){
1211
return;
1212
}
1213
var e = evt.target;
1214
while( e && !/\btimeline\w+Cell\b/.test(e.className) ){
1215
e = e.parentElement;
1216
}
1217
if( e && (e = e.previousElementSibling.querySelector('.tl-nodemark')) ){
1218
evt.preventDefault();
1219
evt.stopPropagation();
1220
focusCacheSet(e.id);
1221
focusVisualize(e.id,false);
1222
}
1223
},false);
1224
window.addEventListener('pageshow',function(evt){
1225
var id = focusCacheGet();
1226
if( !id || !focusVisualize(id,false) ){
1227
if( focusCookieQuery() ){
1228
id = focusDefaultId();
1229
focusCacheSet(id);
1230
focusVisualize(id,false);
1231
}
1232
}
1233
},false);
1234
},false);
1235
}());
1236

Keyboard Shortcuts

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