0
|
1 // mercurial.js - JavaScript utility functions
|
|
2 //
|
|
3 // Rendering of branch DAGs on the client side
|
|
4 // Display of elapsed time
|
|
5 // Show or hide diffstat
|
|
6 //
|
|
7 // Copyright 2008 Dirkjan Ochtman <dirkjan AT ochtman DOT nl>
|
|
8 // Copyright 2006 Alexander Schremmer <alex AT alexanderweb DOT de>
|
|
9 //
|
|
10 // derived from code written by Scott James Remnant <scott@ubuntu.com>
|
|
11 // Copyright 2005 Canonical Ltd.
|
|
12 //
|
|
13 // This software may be used and distributed according to the terms
|
|
14 // of the GNU General Public License, incorporated herein by reference.
|
|
15
|
|
16 var colors = [
|
|
17 [ 1.0, 0.0, 0.0 ],
|
|
18 [ 1.0, 1.0, 0.0 ],
|
|
19 [ 0.0, 1.0, 0.0 ],
|
|
20 [ 0.0, 1.0, 1.0 ],
|
|
21 [ 0.0, 0.0, 1.0 ],
|
|
22 [ 1.0, 0.0, 1.0 ]
|
|
23 ];
|
|
24
|
|
25 function Graph() {
|
|
26
|
|
27 this.canvas = document.getElementById('graph');
|
|
28 this.ctx = this.canvas.getContext('2d');
|
|
29 this.ctx.strokeStyle = 'rgb(0, 0, 0)';
|
|
30 this.ctx.fillStyle = 'rgb(0, 0, 0)';
|
|
31 this.bg = [0, 4];
|
|
32 this.cell = [2, 0];
|
|
33 this.columns = 0;
|
|
34
|
|
35 }
|
|
36
|
|
37 Graph.prototype = {
|
|
38 reset: function() {
|
|
39 this.bg = [0, 4];
|
|
40 this.cell = [2, 0];
|
|
41 this.columns = 0;
|
|
42 },
|
|
43
|
|
44 scale: function(height) {
|
|
45 this.bg_height = height;
|
|
46 this.box_size = Math.floor(this.bg_height / 1.2);
|
|
47 this.cell_height = this.box_size;
|
|
48 },
|
|
49
|
|
50 setColor: function(color, bg, fg) {
|
|
51
|
|
52 // Set the colour.
|
|
53 //
|
|
54 // If color is a string, expect an hexadecimal RGB
|
|
55 // value and apply it unchanged. If color is a number,
|
|
56 // pick a distinct colour based on an internal wheel;
|
|
57 // the bg parameter provides the value that should be
|
|
58 // assigned to the 'zero' colours and the fg parameter
|
|
59 // provides the multiplier that should be applied to
|
|
60 // the foreground colours.
|
|
61 var s;
|
|
62 if(typeof color === "string") {
|
|
63 s = "#" + color;
|
|
64 } else { //typeof color === "number"
|
|
65 color %= colors.length;
|
|
66 var red = (colors[color][0] * fg) || bg;
|
|
67 var green = (colors[color][1] * fg) || bg;
|
|
68 var blue = (colors[color][2] * fg) || bg;
|
|
69 red = Math.round(red * 255);
|
|
70 green = Math.round(green * 255);
|
|
71 blue = Math.round(blue * 255);
|
|
72 s = 'rgb(' + red + ', ' + green + ', ' + blue + ')';
|
|
73 }
|
|
74 this.ctx.strokeStyle = s;
|
|
75 this.ctx.fillStyle = s;
|
|
76 return s;
|
|
77
|
|
78 },
|
|
79
|
|
80 edge: function(x0, y0, x1, y1, color, width) {
|
|
81
|
|
82 this.setColor(color, 0.0, 0.65);
|
|
83 if(width >= 0)
|
|
84 this.ctx.lineWidth = width;
|
|
85 this.ctx.beginPath();
|
|
86 this.ctx.moveTo(x0, y0);
|
|
87 this.ctx.lineTo(x1, y1);
|
|
88 this.ctx.stroke();
|
|
89
|
|
90 },
|
|
91
|
|
92 graphNodeCurrent: function(x, y, radius) {
|
|
93 this.ctx.lineWidth = 2;
|
|
94 this.ctx.beginPath();
|
|
95 this.ctx.arc(x, y, radius * 1.75, 0, Math.PI * 2, true);
|
|
96 this.ctx.stroke();
|
|
97 },
|
|
98
|
|
99 graphNodeClosing: function(x, y, radius) {
|
|
100 this.ctx.fillRect(x - radius, y - 1.5, radius * 2, 3);
|
|
101 },
|
|
102
|
|
103 graphNodeUnstable: function(x, y, radius) {
|
|
104 var x30 = radius * Math.cos(Math.PI / 6);
|
|
105 var y30 = radius * Math.sin(Math.PI / 6);
|
|
106 this.ctx.lineWidth = 2;
|
|
107 this.ctx.beginPath();
|
|
108 this.ctx.moveTo(x, y - radius);
|
|
109 this.ctx.lineTo(x, y + radius);
|
|
110 this.ctx.moveTo(x - x30, y - y30);
|
|
111 this.ctx.lineTo(x + x30, y + y30);
|
|
112 this.ctx.moveTo(x - x30, y + y30);
|
|
113 this.ctx.lineTo(x + x30, y - y30);
|
|
114 this.ctx.stroke();
|
|
115 },
|
|
116
|
|
117 graphNodeObsolete: function(x, y, radius) {
|
|
118 var p45 = radius * Math.cos(Math.PI / 4);
|
|
119 this.ctx.lineWidth = 3;
|
|
120 this.ctx.beginPath();
|
|
121 this.ctx.moveTo(x - p45, y - p45);
|
|
122 this.ctx.lineTo(x + p45, y + p45);
|
|
123 this.ctx.moveTo(x - p45, y + p45);
|
|
124 this.ctx.lineTo(x + p45, y - p45);
|
|
125 this.ctx.stroke();
|
|
126 },
|
|
127
|
|
128 graphNodeNormal: function(x, y, radius) {
|
|
129 this.ctx.beginPath();
|
|
130 this.ctx.arc(x, y, radius, 0, Math.PI * 2, true);
|
|
131 this.ctx.fill();
|
|
132 },
|
|
133
|
|
134 vertex: function(x, y, radius, color, parity, cur) {
|
|
135 this.ctx.save();
|
|
136 this.setColor(color, 0.25, 0.75);
|
|
137 if (cur.graphnode[0] === '@') {
|
|
138 this.graphNodeCurrent(x, y, radius);
|
|
139 }
|
|
140 switch (cur.graphnode.substr(-1)) {
|
|
141 case '_':
|
|
142 this.graphNodeClosing(x, y, radius);
|
|
143 break;
|
|
144 case '*':
|
|
145 this.graphNodeUnstable(x, y, radius);
|
|
146 break;
|
|
147 case 'x':
|
|
148 this.graphNodeObsolete(x, y, radius);
|
|
149 break;
|
|
150 default:
|
|
151 this.graphNodeNormal(x, y, radius);
|
|
152 }
|
|
153 this.ctx.restore();
|
|
154
|
|
155 var left = (this.bg_height - this.box_size) + (this.columns + 1) * this.box_size;
|
|
156 var item = document.querySelector('[data-node="' + cur.node + '"]');
|
|
157 if (item) {
|
|
158 item.style.paddingLeft = left + 'px';
|
|
159 }
|
|
160 },
|
|
161
|
|
162 render: function(data) {
|
|
163
|
|
164 var i, j, cur, line, start, end, color, x, y, x0, y0, x1, y1, column, radius;
|
|
165
|
|
166 var cols = 0;
|
|
167 for (i = 0; i < data.length; i++) {
|
|
168 cur = data[i];
|
|
169 for (j = 0; j < cur.edges.length; j++) {
|
|
170 line = cur.edges[j];
|
|
171 cols = Math.max(cols, line[0], line[1]);
|
|
172 }
|
|
173 }
|
|
174 this.canvas.width = (cols + 1) * this.bg_height;
|
|
175 this.canvas.height = (data.length + 1) * this.bg_height - 27;
|
|
176
|
|
177 for (i = 0; i < data.length; i++) {
|
|
178
|
|
179 var parity = i % 2;
|
|
180 this.cell[1] += this.bg_height;
|
|
181 this.bg[1] += this.bg_height;
|
|
182
|
|
183 cur = data[i];
|
|
184 var fold = false;
|
|
185
|
|
186 var prevWidth = this.ctx.lineWidth;
|
|
187 for (j = 0; j < cur.edges.length; j++) {
|
|
188
|
|
189 line = cur.edges[j];
|
|
190 start = line[0];
|
|
191 end = line[1];
|
|
192 color = line[2];
|
|
193 var width = line[3];
|
|
194 if(width < 0)
|
|
195 width = prevWidth;
|
|
196 var branchcolor = line[4];
|
|
197 if(branchcolor)
|
|
198 color = branchcolor;
|
|
199
|
|
200 if (end > this.columns || start > this.columns) {
|
|
201 this.columns += 1;
|
|
202 }
|
|
203
|
|
204 if (start === this.columns && start > end) {
|
|
205 fold = true;
|
|
206 }
|
|
207
|
|
208 x0 = this.cell[0] + this.box_size * start + this.box_size / 2;
|
|
209 y0 = this.bg[1] - this.bg_height / 2;
|
|
210 x1 = this.cell[0] + this.box_size * end + this.box_size / 2;
|
|
211 y1 = this.bg[1] + this.bg_height / 2;
|
|
212
|
|
213 this.edge(x0, y0, x1, y1, color, width);
|
|
214
|
|
215 }
|
|
216 this.ctx.lineWidth = prevWidth;
|
|
217
|
|
218 // Draw the revision node in the right column
|
|
219
|
|
220 column = cur.vertex[0];
|
|
221 color = cur.vertex[1];
|
|
222
|
|
223 radius = this.box_size / 8;
|
|
224 x = this.cell[0] + this.box_size * column + this.box_size / 2;
|
|
225 y = this.bg[1] - this.bg_height / 2;
|
|
226 this.vertex(x, y, radius, color, parity, cur);
|
|
227
|
|
228 if (fold) this.columns -= 1;
|
|
229
|
|
230 }
|
|
231
|
|
232 }
|
|
233
|
|
234 };
|
|
235
|
|
236
|
|
237 function process_dates(parentSelector){
|
|
238
|
|
239 // derived from code from mercurial/templatefilter.py
|
|
240
|
|
241 var scales = {
|
|
242 'year': 365 * 24 * 60 * 60,
|
|
243 'month': 30 * 24 * 60 * 60,
|
|
244 'week': 7 * 24 * 60 * 60,
|
|
245 'day': 24 * 60 * 60,
|
|
246 'hour': 60 * 60,
|
|
247 'minute': 60,
|
|
248 'second': 1
|
|
249 };
|
|
250
|
|
251 function format(count, string){
|
|
252 var ret = count + ' ' + string;
|
|
253 if (count > 1){
|
|
254 ret = ret + 's';
|
|
255 }
|
|
256 return ret;
|
|
257 }
|
|
258
|
|
259 function shortdate(date){
|
|
260 var ret = date.getFullYear() + '-';
|
|
261 // getMonth() gives a 0-11 result
|
|
262 var month = date.getMonth() + 1;
|
|
263 if (month <= 9){
|
|
264 ret += '0' + month;
|
|
265 } else {
|
|
266 ret += month;
|
|
267 }
|
|
268 ret += '-';
|
|
269 var day = date.getDate();
|
|
270 if (day <= 9){
|
|
271 ret += '0' + day;
|
|
272 } else {
|
|
273 ret += day;
|
|
274 }
|
|
275 return ret;
|
|
276 }
|
|
277
|
|
278 function age(datestr){
|
|
279 var now = new Date();
|
|
280 var once = new Date(datestr);
|
|
281 if (isNaN(once.getTime())){
|
|
282 // parsing error
|
|
283 return datestr;
|
|
284 }
|
|
285
|
|
286 var delta = Math.floor((now.getTime() - once.getTime()) / 1000);
|
|
287
|
|
288 var future = false;
|
|
289 if (delta < 0){
|
|
290 future = true;
|
|
291 delta = -delta;
|
|
292 if (delta > (30 * scales.year)){
|
|
293 return "in the distant future";
|
|
294 }
|
|
295 }
|
|
296
|
|
297 if (delta > (2 * scales.year)){
|
|
298 return shortdate(once);
|
|
299 }
|
|
300
|
|
301 for (var unit in scales){
|
|
302 if (!scales.hasOwnProperty(unit)) { continue; }
|
|
303 var s = scales[unit];
|
|
304 var n = Math.floor(delta / s);
|
|
305 if ((n >= 2) || (s === 1)){
|
|
306 if (future){
|
|
307 return format(n, unit) + ' from now';
|
|
308 } else {
|
|
309 return format(n, unit) + ' ago';
|
|
310 }
|
|
311 }
|
|
312 }
|
|
313 }
|
|
314
|
|
315 var nodes = document.querySelectorAll((parentSelector || '') + ' .age');
|
|
316 var dateclass = new RegExp('\\bdate\\b');
|
|
317 for (var i=0; i<nodes.length; ++i){
|
|
318 var node = nodes[i];
|
|
319 var classes = node.className;
|
|
320 var agevalue = age(node.textContent);
|
|
321 if (dateclass.test(classes)){
|
|
322 // We want both: date + (age)
|
|
323 node.textContent += ' ('+agevalue+')';
|
|
324 } else {
|
|
325 node.title = node.textContent;
|
|
326 node.textContent = agevalue;
|
|
327 }
|
|
328 }
|
|
329 }
|
|
330
|
|
331 function toggleDiffstat(event) {
|
|
332 var curdetails = document.getElementById('diffstatdetails').style.display;
|
|
333 var curexpand = curdetails === 'none' ? 'inline' : 'none';
|
|
334 document.getElementById('diffstatdetails').style.display = curexpand;
|
|
335 document.getElementById('diffstatexpand').style.display = curdetails;
|
|
336 event.preventDefault();
|
|
337 }
|
|
338
|
|
339 function toggleLinewrap(event) {
|
|
340 function getLinewrap() {
|
|
341 var nodes = document.getElementsByClassName('sourcelines');
|
|
342 // if there are no such nodes, error is thrown here
|
|
343 return nodes[0].classList.contains('wrap');
|
|
344 }
|
|
345
|
|
346 function setLinewrap(enable) {
|
|
347 var nodes = document.getElementsByClassName('sourcelines');
|
|
348 var i;
|
|
349 for (i = 0; i < nodes.length; i++) {
|
|
350 if (enable) {
|
|
351 nodes[i].classList.add('wrap');
|
|
352 } else {
|
|
353 nodes[i].classList.remove('wrap');
|
|
354 }
|
|
355 }
|
|
356
|
|
357 var links = document.getElementsByClassName('linewraplink');
|
|
358 for (i = 0; i < links.length; i++) {
|
|
359 links[i].innerHTML = enable ? 'on' : 'off';
|
|
360 }
|
|
361 }
|
|
362
|
|
363 setLinewrap(!getLinewrap());
|
|
364 event.preventDefault();
|
|
365 }
|
|
366
|
|
367 function format(str, replacements) {
|
|
368 return str.replace(/%(\w+)%/g, function(match, p1) {
|
|
369 return String(replacements[p1]);
|
|
370 });
|
|
371 }
|
|
372
|
|
373 function makeRequest(url, method, onstart, onsuccess, onerror, oncomplete) {
|
|
374 var xhr = new XMLHttpRequest();
|
|
375 xhr.onreadystatechange = function() {
|
|
376 if (xhr.readyState === 4) {
|
|
377 try {
|
|
378 if (xhr.status === 200) {
|
|
379 onsuccess(xhr.responseText);
|
|
380 } else {
|
|
381 throw 'server error';
|
|
382 }
|
|
383 } catch (e) {
|
|
384 onerror(e);
|
|
385 } finally {
|
|
386 oncomplete();
|
|
387 }
|
|
388 }
|
|
389 };
|
|
390
|
|
391 xhr.open(method, url);
|
|
392 xhr.overrideMimeType("text/xhtml; charset=" + document.characterSet.toLowerCase());
|
|
393 xhr.send();
|
|
394 onstart();
|
|
395 return xhr;
|
|
396 }
|
|
397
|
|
398 function removeByClassName(className) {
|
|
399 var nodes = document.getElementsByClassName(className);
|
|
400 while (nodes.length) {
|
|
401 nodes[0].parentNode.removeChild(nodes[0]);
|
|
402 }
|
|
403 }
|
|
404
|
|
405 function docFromHTML(html) {
|
|
406 var doc = document.implementation.createHTMLDocument('');
|
|
407 doc.documentElement.innerHTML = html;
|
|
408 return doc;
|
|
409 }
|
|
410
|
|
411 function appendFormatHTML(element, formatStr, replacements) {
|
|
412 element.insertAdjacentHTML('beforeend', format(formatStr, replacements));
|
|
413 }
|
|
414
|
|
415 function adoptChildren(from, to) {
|
|
416 var nodes = from.children;
|
|
417 var curClass = 'c' + Date.now();
|
|
418 while (nodes.length) {
|
|
419 var node = nodes[0];
|
|
420 node = document.adoptNode(node);
|
|
421 node.classList.add(curClass);
|
|
422 to.appendChild(node);
|
|
423 }
|
|
424 process_dates('.' + curClass);
|
|
425 }
|
|
426
|
|
427 function ajaxScrollInit(urlFormat,
|
|
428 nextPageVar,
|
|
429 nextPageVarGet,
|
|
430 containerSelector,
|
|
431 messageFormat,
|
|
432 mode) {
|
|
433 var updateInitiated = false;
|
|
434 var container = document.querySelector(containerSelector);
|
|
435
|
|
436 function scrollHandler() {
|
|
437 if (updateInitiated) {
|
|
438 return;
|
|
439 }
|
|
440
|
|
441 var scrollHeight = document.documentElement.scrollHeight;
|
|
442 var clientHeight = document.documentElement.clientHeight;
|
|
443 var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
|
|
444
|
|
445 if (scrollHeight - (scrollTop + clientHeight) < 50) {
|
|
446 updateInitiated = true;
|
|
447 removeByClassName('scroll-loading-error');
|
|
448 container.lastElementChild.classList.add('scroll-separator');
|
|
449
|
|
450 if (!nextPageVar) {
|
|
451 var message = {
|
|
452 'class': 'scroll-loading-info',
|
|
453 text: 'No more entries'
|
|
454 };
|
|
455 appendFormatHTML(container, messageFormat, message);
|
|
456 return;
|
|
457 }
|
|
458
|
|
459 makeRequest(
|
|
460 format(urlFormat, {next: nextPageVar}),
|
|
461 'GET',
|
|
462 function onstart() {
|
|
463 var message = {
|
|
464 'class': 'scroll-loading',
|
|
465 text: 'Loading...'
|
|
466 };
|
|
467 appendFormatHTML(container, messageFormat, message);
|
|
468 },
|
|
469 function onsuccess(htmlText) {
|
|
470 var doc = docFromHTML(htmlText);
|
|
471
|
|
472 if (mode === 'graph') {
|
|
473 var graph = window.graph;
|
|
474 var dataStr = htmlText.match(/^\s*var data = (.*);$/m)[1];
|
|
475 var data = JSON.parse(dataStr);
|
|
476 graph.reset();
|
|
477 adoptChildren(doc.querySelector('#graphnodes'), container.querySelector('#graphnodes'));
|
|
478 graph.render(data);
|
|
479 } else {
|
|
480 adoptChildren(doc.querySelector(containerSelector), container);
|
|
481 }
|
|
482
|
|
483 nextPageVar = nextPageVarGet(htmlText);
|
|
484 },
|
|
485 function onerror(errorText) {
|
|
486 var message = {
|
|
487 'class': 'scroll-loading-error',
|
|
488 text: 'Error: ' + errorText
|
|
489 };
|
|
490 appendFormatHTML(container, messageFormat, message);
|
|
491 },
|
|
492 function oncomplete() {
|
|
493 removeByClassName('scroll-loading');
|
|
494 updateInitiated = false;
|
|
495 scrollHandler();
|
|
496 }
|
|
497 );
|
|
498 }
|
|
499 }
|
|
500
|
|
501 window.addEventListener('scroll', scrollHandler);
|
|
502 window.addEventListener('resize', scrollHandler);
|
|
503 scrollHandler();
|
|
504 }
|
|
505
|
|
506 function renderDiffOptsForm() {
|
|
507 // We use URLSearchParams for query string manipulation. Old browsers don't
|
|
508 // support this API.
|
|
509 if (!("URLSearchParams" in window)) {
|
|
510 return;
|
|
511 }
|
|
512
|
|
513 var form = document.getElementById("diffopts-form");
|
|
514
|
|
515 var KEYS = [
|
|
516 "ignorews",
|
|
517 "ignorewsamount",
|
|
518 "ignorewseol",
|
|
519 "ignoreblanklines",
|
|
520 ];
|
|
521
|
|
522 var urlParams = new window.URLSearchParams(window.location.search);
|
|
523
|
|
524 function updateAndRefresh(e) {
|
|
525 var checkbox = e.target;
|
|
526 var name = checkbox.id.substr(0, checkbox.id.indexOf("-"));
|
|
527 urlParams.set(name, checkbox.checked ? "1" : "0");
|
|
528 window.location.search = urlParams.toString();
|
|
529 }
|
|
530
|
|
531 var allChecked = form.getAttribute("data-ignorews") === "1";
|
|
532
|
|
533 for (var i = 0; i < KEYS.length; i++) {
|
|
534 var key = KEYS[i];
|
|
535
|
|
536 var checkbox = document.getElementById(key + "-checkbox");
|
|
537 if (!checkbox) {
|
|
538 continue;
|
|
539 }
|
|
540
|
|
541 var currentValue = form.getAttribute("data-" + key);
|
|
542 checkbox.checked = currentValue !== "0";
|
|
543
|
|
544 // ignorews implies ignorewsamount and ignorewseol.
|
|
545 if (allChecked && (key === "ignorewsamount" || key === "ignorewseol")) {
|
|
546 checkbox.checked = true;
|
|
547 checkbox.disabled = true;
|
|
548 }
|
|
549
|
|
550 checkbox.addEventListener("change", updateAndRefresh, false);
|
|
551 }
|
|
552
|
|
553 form.style.display = 'block';
|
|
554 }
|
|
555
|
|
556 function addDiffStatToggle() {
|
|
557 var els = document.getElementsByClassName("diffstattoggle");
|
|
558
|
|
559 for (var i = 0; i < els.length; i++) {
|
|
560 els[i].addEventListener("click", toggleDiffstat, false);
|
|
561 }
|
|
562 }
|
|
563
|
|
564 function addLineWrapToggle() {
|
|
565 var els = document.getElementsByClassName("linewraptoggle");
|
|
566
|
|
567 for (var i = 0; i < els.length; i++) {
|
|
568 var nodes = els[i].getElementsByClassName("linewraplink");
|
|
569
|
|
570 for (var j = 0; j < nodes.length; j++) {
|
|
571 nodes[j].addEventListener("click", toggleLinewrap, false);
|
|
572 }
|
|
573 }
|
|
574 }
|
|
575
|
|
576 document.addEventListener('DOMContentLoaded', function() {
|
|
577 process_dates();
|
|
578 addDiffStatToggle();
|
|
579 addLineWrapToggle();
|
|
580 }, false);
|