|
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, "&") |
|
672
|
.replace(/</g, "<") |
|
673
|
.replace(/>/g, ">") |
|
674
|
.replace(/"/g, """) |
|
675
|
.replace(/'/g, "'"); |
|
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
|
|