| 
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);
 |