changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > infra > home / templates/static/mercurial.js

changeset 53: d25f982fb8a6
author: Richard Westhaver <ellis@rwest.io>
date: Sat, 20 Jul 2024 22:31:54 -0400
permissions: -rw-r--r--
description: init vc
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);