rev |
line source |
pankso@282
|
1 // mercurial.js - JavaScript utility functions
|
pankso@282
|
2 //
|
pankso@282
|
3 // Rendering of branch DAGs on the client side
|
pankso@282
|
4 // Display of elapsed time
|
pankso@282
|
5 // Show or hide diffstat
|
pankso@282
|
6 //
|
pankso@282
|
7 // Copyright 2008 Dirkjan Ochtman <dirkjan AT ochtman DOT nl>
|
pankso@282
|
8 // Copyright 2006 Alexander Schremmer <alex AT alexanderweb DOT de>
|
pankso@282
|
9 //
|
pankso@282
|
10 // derived from code written by Scott James Remnant <scott@ubuntu.com>
|
pankso@282
|
11 // Copyright 2005 Canonical Ltd.
|
pankso@282
|
12 //
|
pankso@282
|
13 // This software may be used and distributed according to the terms
|
pankso@282
|
14 // of the GNU General Public License, incorporated herein by reference.
|
pankso@282
|
15
|
pankso@282
|
16 var colors = [
|
pankso@282
|
17 [ 1.0, 0.0, 0.0 ],
|
pankso@282
|
18 [ 1.0, 1.0, 0.0 ],
|
pankso@282
|
19 [ 0.0, 1.0, 0.0 ],
|
pankso@282
|
20 [ 0.0, 1.0, 1.0 ],
|
pankso@282
|
21 [ 0.0, 0.0, 1.0 ],
|
pankso@282
|
22 [ 1.0, 0.0, 1.0 ]
|
pankso@282
|
23 ];
|
pankso@282
|
24
|
pankso@282
|
25 function Graph() {
|
pankso@282
|
26
|
pankso@282
|
27 this.canvas = document.getElementById('graph');
|
pankso@282
|
28 if (window.G_vmlCanvasManager) this.canvas = window.G_vmlCanvasManager.initElement(this.canvas);
|
pankso@282
|
29 this.ctx = this.canvas.getContext('2d');
|
pankso@282
|
30 this.ctx.strokeStyle = 'rgb(0, 0, 0)';
|
pankso@282
|
31 this.ctx.fillStyle = 'rgb(0, 0, 0)';
|
pankso@282
|
32 this.cur = [0, 0];
|
pankso@282
|
33 this.line_width = 3;
|
pankso@282
|
34 this.bg = [0, 4];
|
pankso@282
|
35 this.cell = [2, 0];
|
pankso@282
|
36 this.columns = 0;
|
pankso@282
|
37 this.revlink = '';
|
pankso@282
|
38
|
pankso@282
|
39 this.reset = function() {
|
pankso@282
|
40 this.bg = [0, 4];
|
pankso@282
|
41 this.cell = [2, 0];
|
pankso@282
|
42 this.columns = 0;
|
pankso@282
|
43 document.getElementById('nodebgs').innerHTML = '';
|
pankso@282
|
44 document.getElementById('graphnodes').innerHTML = '';
|
pankso@282
|
45 }
|
pankso@282
|
46
|
pankso@282
|
47 this.scale = function(height) {
|
pankso@282
|
48 this.bg_height = height;
|
pankso@282
|
49 this.box_size = Math.floor(this.bg_height / 1.2);
|
pankso@282
|
50 this.cell_height = this.box_size;
|
pankso@282
|
51 }
|
pankso@282
|
52
|
pankso@282
|
53 function colorPart(num) {
|
pankso@282
|
54 num *= 255
|
pankso@282
|
55 num = num < 0 ? 0 : num;
|
pankso@282
|
56 num = num > 255 ? 255 : num;
|
pankso@282
|
57 var digits = Math.round(num).toString(16);
|
pankso@282
|
58 if (num < 16) {
|
pankso@282
|
59 return '0' + digits;
|
pankso@282
|
60 } else {
|
pankso@282
|
61 return digits;
|
pankso@282
|
62 }
|
pankso@282
|
63 }
|
pankso@282
|
64
|
pankso@282
|
65 this.setColor = function(color, bg, fg) {
|
pankso@282
|
66
|
pankso@282
|
67 // Set the colour.
|
pankso@282
|
68 //
|
pankso@282
|
69 // If color is a string, expect an hexadecimal RGB
|
pankso@282
|
70 // value and apply it unchanged. If color is a number,
|
pankso@282
|
71 // pick a distinct colour based on an internal wheel;
|
pankso@282
|
72 // the bg parameter provides the value that should be
|
pankso@282
|
73 // assigned to the 'zero' colours and the fg parameter
|
pankso@282
|
74 // provides the multiplier that should be applied to
|
pankso@282
|
75 // the foreground colours.
|
pankso@282
|
76 var s;
|
pankso@282
|
77 if(typeof color == "string") {
|
pankso@282
|
78 s = "#" + color;
|
pankso@282
|
79 } else { //typeof color == "number"
|
pankso@282
|
80 color %= colors.length;
|
pankso@282
|
81 var red = (colors[color][0] * fg) || bg;
|
pankso@282
|
82 var green = (colors[color][1] * fg) || bg;
|
pankso@282
|
83 var blue = (colors[color][2] * fg) || bg;
|
pankso@282
|
84 red = Math.round(red * 255);
|
pankso@282
|
85 green = Math.round(green * 255);
|
pankso@282
|
86 blue = Math.round(blue * 255);
|
pankso@282
|
87 s = 'rgb(' + red + ', ' + green + ', ' + blue + ')';
|
pankso@282
|
88 }
|
pankso@282
|
89 this.ctx.strokeStyle = s;
|
pankso@282
|
90 this.ctx.fillStyle = s;
|
pankso@282
|
91 return s;
|
pankso@282
|
92
|
pankso@282
|
93 }
|
pankso@282
|
94
|
pankso@282
|
95 this.edge = function(x0, y0, x1, y1, color, width) {
|
pankso@282
|
96
|
pankso@282
|
97 this.setColor(color, 0.0, 0.65);
|
pankso@282
|
98 if(width >= 0)
|
pankso@282
|
99 this.ctx.lineWidth = width;
|
pankso@282
|
100 this.ctx.beginPath();
|
pankso@282
|
101 this.ctx.moveTo(x0, y0);
|
pankso@282
|
102 this.ctx.lineTo(x1, y1);
|
pankso@282
|
103 this.ctx.stroke();
|
pankso@282
|
104
|
pankso@282
|
105 }
|
pankso@282
|
106
|
pankso@282
|
107 this.render = function(data) {
|
pankso@282
|
108
|
pankso@282
|
109 var backgrounds = '';
|
pankso@282
|
110 var nodedata = '';
|
pankso@282
|
111
|
pankso@282
|
112 for (var i in data) {
|
pankso@282
|
113
|
pankso@282
|
114 var parity = i % 2;
|
pankso@282
|
115 this.cell[1] += this.bg_height;
|
pankso@282
|
116 this.bg[1] += this.bg_height;
|
pankso@282
|
117
|
pankso@282
|
118 var cur = data[i];
|
pankso@282
|
119 var node = cur[1];
|
pankso@282
|
120 var edges = cur[2];
|
pankso@282
|
121 var fold = false;
|
pankso@282
|
122
|
pankso@282
|
123 var prevWidth = this.ctx.lineWidth;
|
pankso@282
|
124 for (var j in edges) {
|
pankso@282
|
125
|
pankso@282
|
126 line = edges[j];
|
pankso@282
|
127 start = line[0];
|
pankso@282
|
128 end = line[1];
|
pankso@282
|
129 color = line[2];
|
pankso@282
|
130 var width = line[3];
|
pankso@282
|
131 if(width < 0)
|
pankso@282
|
132 width = prevWidth;
|
pankso@282
|
133 var branchcolor = line[4];
|
pankso@282
|
134 if(branchcolor)
|
pankso@282
|
135 color = branchcolor;
|
pankso@282
|
136
|
pankso@282
|
137 if (end > this.columns || start > this.columns) {
|
pankso@282
|
138 this.columns += 1;
|
pankso@282
|
139 }
|
pankso@282
|
140
|
pankso@282
|
141 if (start == this.columns && start > end) {
|
pankso@282
|
142 var fold = true;
|
pankso@282
|
143 }
|
pankso@282
|
144
|
pankso@282
|
145 x0 = this.cell[0] + this.box_size * start + this.box_size / 2;
|
pankso@282
|
146 y0 = this.bg[1] - this.bg_height / 2;
|
pankso@282
|
147 x1 = this.cell[0] + this.box_size * end + this.box_size / 2;
|
pankso@282
|
148 y1 = this.bg[1] + this.bg_height / 2;
|
pankso@282
|
149
|
pankso@282
|
150 this.edge(x0, y0, x1, y1, color, width);
|
pankso@282
|
151
|
pankso@282
|
152 }
|
pankso@282
|
153 this.ctx.lineWidth = prevWidth;
|
pankso@282
|
154
|
pankso@282
|
155 // Draw the revision node in the right column
|
pankso@282
|
156
|
pankso@282
|
157 column = node[0]
|
pankso@282
|
158 color = node[1]
|
pankso@282
|
159
|
pankso@282
|
160 radius = this.box_size / 8;
|
pankso@282
|
161 x = this.cell[0] + this.box_size * column + this.box_size / 2;
|
pankso@282
|
162 y = this.bg[1] - this.bg_height / 2;
|
pankso@282
|
163 var add = this.vertex(x, y, color, parity, cur);
|
pankso@282
|
164 backgrounds += add[0];
|
pankso@282
|
165 nodedata += add[1];
|
pankso@282
|
166
|
pankso@282
|
167 if (fold) this.columns -= 1;
|
pankso@282
|
168
|
pankso@282
|
169 }
|
pankso@282
|
170
|
pankso@282
|
171 document.getElementById('nodebgs').innerHTML += backgrounds;
|
pankso@282
|
172 document.getElementById('graphnodes').innerHTML += nodedata;
|
pankso@282
|
173
|
pankso@282
|
174 }
|
pankso@282
|
175
|
pankso@282
|
176 }
|
pankso@282
|
177
|
pankso@282
|
178
|
pankso@282
|
179 function process_dates(parentSelector){
|
pankso@282
|
180
|
pankso@282
|
181 // derived from code from mercurial/templatefilter.py
|
pankso@282
|
182
|
pankso@282
|
183 var scales = {
|
pankso@282
|
184 'year': 365 * 24 * 60 * 60,
|
pankso@282
|
185 'month': 30 * 24 * 60 * 60,
|
pankso@282
|
186 'week': 7 * 24 * 60 * 60,
|
pankso@282
|
187 'day': 24 * 60 * 60,
|
pankso@282
|
188 'hour': 60 * 60,
|
pankso@282
|
189 'minute': 60,
|
pankso@282
|
190 'second': 1
|
pankso@282
|
191 };
|
pankso@282
|
192
|
pankso@282
|
193 function format(count, string){
|
pankso@282
|
194 var ret = count + ' ' + string;
|
pankso@282
|
195 if (count > 1){
|
pankso@282
|
196 ret = ret + 's';
|
pankso@282
|
197 }
|
pankso@282
|
198 return ret;
|
pankso@282
|
199 }
|
pankso@282
|
200
|
pankso@282
|
201 function shortdate(date){
|
pankso@282
|
202 var ret = date.getFullYear() + '-';
|
pankso@282
|
203 // getMonth() gives a 0-11 result
|
pankso@282
|
204 var month = date.getMonth() + 1;
|
pankso@282
|
205 if (month <= 9){
|
pankso@282
|
206 ret += '0' + month;
|
pankso@282
|
207 } else {
|
pankso@282
|
208 ret += month;
|
pankso@282
|
209 }
|
pankso@282
|
210 ret += '-';
|
pankso@282
|
211 var day = date.getDate();
|
pankso@282
|
212 if (day <= 9){
|
pankso@282
|
213 ret += '0' + day;
|
pankso@282
|
214 } else {
|
pankso@282
|
215 ret += day;
|
pankso@282
|
216 }
|
pankso@282
|
217 return ret;
|
pankso@282
|
218 }
|
pankso@282
|
219
|
pankso@282
|
220 function age(datestr){
|
pankso@282
|
221 var now = new Date();
|
pankso@282
|
222 var once = new Date(datestr);
|
pankso@282
|
223 if (isNaN(once.getTime())){
|
pankso@282
|
224 // parsing error
|
pankso@282
|
225 return datestr;
|
pankso@282
|
226 }
|
pankso@282
|
227
|
pankso@282
|
228 var delta = Math.floor((now.getTime() - once.getTime()) / 1000);
|
pankso@282
|
229
|
pankso@282
|
230 var future = false;
|
pankso@282
|
231 if (delta < 0){
|
pankso@282
|
232 future = true;
|
pankso@282
|
233 delta = -delta;
|
pankso@282
|
234 if (delta > (30 * scales.year)){
|
pankso@282
|
235 return "in the distant future";
|
pankso@282
|
236 }
|
pankso@282
|
237 }
|
pankso@282
|
238
|
pankso@282
|
239 if (delta > (2 * scales.year)){
|
pankso@282
|
240 return shortdate(once);
|
pankso@282
|
241 }
|
pankso@282
|
242
|
pankso@282
|
243 for (unit in scales){
|
pankso@282
|
244 var s = scales[unit];
|
pankso@282
|
245 var n = Math.floor(delta / s);
|
pankso@282
|
246 if ((n >= 2) || (s == 1)){
|
pankso@282
|
247 if (future){
|
pankso@282
|
248 return format(n, unit) + ' from now';
|
pankso@282
|
249 } else {
|
pankso@282
|
250 return format(n, unit) + ' ago';
|
pankso@282
|
251 }
|
pankso@282
|
252 }
|
pankso@282
|
253 }
|
pankso@282
|
254 }
|
pankso@282
|
255
|
pankso@282
|
256 var nodes = document.querySelectorAll((parentSelector || '') + ' .age');
|
pankso@282
|
257 var dateclass = new RegExp('\\bdate\\b');
|
pankso@282
|
258 for (var i=0; i<nodes.length; ++i){
|
pankso@282
|
259 var node = nodes[i];
|
pankso@282
|
260 var classes = node.className;
|
pankso@282
|
261 var agevalue = age(node.textContent);
|
pankso@282
|
262 if (dateclass.test(classes)){
|
pankso@282
|
263 // We want both: date + (age)
|
pankso@282
|
264 node.textContent += ' ('+agevalue+')';
|
pankso@282
|
265 } else {
|
pankso@282
|
266 node.title = node.textContent;
|
pankso@282
|
267 node.textContent = agevalue;
|
pankso@282
|
268 }
|
pankso@282
|
269 }
|
pankso@282
|
270 }
|
pankso@282
|
271
|
pankso@282
|
272 function toggleDiffstat() {
|
pankso@282
|
273 var curdetails = document.getElementById('diffstatdetails').style.display;
|
pankso@282
|
274 var curexpand = curdetails == 'none' ? 'inline' : 'none';
|
pankso@282
|
275 document.getElementById('diffstatdetails').style.display = curexpand;
|
pankso@282
|
276 document.getElementById('diffstatexpand').style.display = curdetails;
|
pankso@282
|
277 }
|
pankso@282
|
278
|
pankso@282
|
279 function toggleLinewrap() {
|
pankso@282
|
280 function getLinewrap() {
|
pankso@282
|
281 var nodes = document.getElementsByClassName('sourcelines');
|
pankso@282
|
282 // if there are no such nodes, error is thrown here
|
pankso@282
|
283 return nodes[0].classList.contains('wrap');
|
pankso@282
|
284 }
|
pankso@282
|
285
|
pankso@282
|
286 function setLinewrap(enable) {
|
pankso@282
|
287 var nodes = document.getElementsByClassName('sourcelines');
|
pankso@282
|
288 for (var i = 0; i < nodes.length; i++) {
|
pankso@282
|
289 if (enable) {
|
pankso@282
|
290 nodes[i].classList.add('wrap');
|
pankso@282
|
291 } else {
|
pankso@282
|
292 nodes[i].classList.remove('wrap');
|
pankso@282
|
293 }
|
pankso@282
|
294 }
|
pankso@282
|
295
|
pankso@282
|
296 var links = document.getElementsByClassName('linewraplink');
|
pankso@282
|
297 for (var i = 0; i < links.length; i++) {
|
pankso@282
|
298 links[i].innerHTML = enable ? 'on' : 'off';
|
pankso@282
|
299 }
|
pankso@282
|
300 }
|
pankso@282
|
301
|
pankso@282
|
302 setLinewrap(!getLinewrap());
|
pankso@282
|
303 }
|
pankso@282
|
304
|
pankso@282
|
305 function format(str, replacements) {
|
pankso@282
|
306 return str.replace(/%(\w+)%/g, function(match, p1) {
|
pankso@282
|
307 return String(replacements[p1]);
|
pankso@282
|
308 });
|
pankso@282
|
309 }
|
pankso@282
|
310
|
pankso@282
|
311 function makeRequest(url, method, onstart, onsuccess, onerror, oncomplete) {
|
pankso@282
|
312 xfr = new XMLHttpRequest();
|
pankso@282
|
313 xfr.onreadystatechange = function() {
|
pankso@282
|
314 if (xfr.readyState === 4) {
|
pankso@282
|
315 try {
|
pankso@282
|
316 if (xfr.status === 200) {
|
pankso@282
|
317 onsuccess(xfr.responseText);
|
pankso@282
|
318 } else {
|
pankso@282
|
319 throw 'server error';
|
pankso@282
|
320 }
|
pankso@282
|
321 } catch (e) {
|
pankso@282
|
322 onerror(e);
|
pankso@282
|
323 } finally {
|
pankso@282
|
324 oncomplete();
|
pankso@282
|
325 }
|
pankso@282
|
326 }
|
pankso@282
|
327 };
|
pankso@282
|
328
|
pankso@282
|
329 xfr.open(method, url);
|
pankso@282
|
330 xfr.overrideMimeType("text/xhtml; charset=" + document.characterSet.toLowerCase());
|
pankso@282
|
331 xfr.send();
|
pankso@282
|
332 onstart();
|
pankso@282
|
333 return xfr;
|
pankso@282
|
334 }
|
pankso@282
|
335
|
pankso@282
|
336 function removeByClassName(className) {
|
pankso@282
|
337 var nodes = document.getElementsByClassName(className);
|
pankso@282
|
338 while (nodes.length) {
|
pankso@282
|
339 nodes[0].parentNode.removeChild(nodes[0]);
|
pankso@282
|
340 }
|
pankso@282
|
341 }
|
pankso@282
|
342
|
pankso@282
|
343 function docFromHTML(html) {
|
pankso@282
|
344 var doc = document.implementation.createHTMLDocument('');
|
pankso@282
|
345 doc.documentElement.innerHTML = html;
|
pankso@282
|
346 return doc;
|
pankso@282
|
347 }
|
pankso@282
|
348
|
pankso@282
|
349 function appendFormatHTML(element, formatStr, replacements) {
|
pankso@282
|
350 element.insertAdjacentHTML('beforeend', format(formatStr, replacements));
|
pankso@282
|
351 }
|
pankso@282
|
352
|
pankso@282
|
353 function ajaxScrollInit(urlFormat,
|
pankso@282
|
354 nextPageVar,
|
pankso@282
|
355 nextPageVarGet,
|
pankso@282
|
356 containerSelector,
|
pankso@282
|
357 messageFormat,
|
pankso@282
|
358 mode) {
|
pankso@282
|
359 updateInitiated = false;
|
pankso@282
|
360 container = document.querySelector(containerSelector);
|
pankso@282
|
361
|
pankso@282
|
362 function scrollHandler() {
|
pankso@282
|
363 if (updateInitiated) {
|
pankso@282
|
364 return;
|
pankso@282
|
365 }
|
pankso@282
|
366
|
pankso@282
|
367 var scrollHeight = document.documentElement.scrollHeight;
|
pankso@282
|
368 var clientHeight = document.documentElement.clientHeight;
|
pankso@282
|
369 var scrollTop = document.body.scrollTop
|
pankso@282
|
370 || document.documentElement.scrollTop;
|
pankso@282
|
371
|
pankso@282
|
372 if (scrollHeight - (scrollTop + clientHeight) < 50) {
|
pankso@282
|
373 updateInitiated = true;
|
pankso@282
|
374 removeByClassName('scroll-loading-error');
|
pankso@282
|
375 container.lastElementChild.classList.add('scroll-separator');
|
pankso@282
|
376
|
pankso@282
|
377 if (!nextPageVar) {
|
pankso@282
|
378 var message = {
|
pankso@282
|
379 class: 'scroll-loading-info',
|
pankso@282
|
380 text: 'No more entries'
|
pankso@282
|
381 };
|
pankso@282
|
382 appendFormatHTML(container, messageFormat, message);
|
pankso@282
|
383 return;
|
pankso@282
|
384 }
|
pankso@282
|
385
|
pankso@282
|
386 makeRequest(
|
pankso@282
|
387 format(urlFormat, {next: nextPageVar}),
|
pankso@282
|
388 'GET',
|
pankso@282
|
389 function onstart() {
|
pankso@282
|
390 var message = {
|
pankso@282
|
391 class: 'scroll-loading',
|
pankso@282
|
392 text: 'Loading...'
|
pankso@282
|
393 };
|
pankso@282
|
394 appendFormatHTML(container, messageFormat, message);
|
pankso@282
|
395 },
|
pankso@282
|
396 function onsuccess(htmlText) {
|
pankso@282
|
397 if (mode == 'graph') {
|
pankso@282
|
398 var addHeight = htmlText.match(/^\s*<canvas id="graph".*height="(\d+)"><\/canvas>$/m)[1];
|
pankso@282
|
399 addHeight = parseInt(addHeight);
|
pankso@282
|
400 graph.canvas.height = addHeight;
|
pankso@282
|
401
|
pankso@282
|
402 var dataStr = htmlText.match(/^\s*var data = (.*);$/m)[1];
|
pankso@282
|
403 var data = JSON.parse(dataStr);
|
pankso@282
|
404 if (data.length < nextPageVar) {
|
pankso@282
|
405 nextPageVar = undefined;
|
pankso@282
|
406 }
|
pankso@282
|
407 graph.reset();
|
pankso@282
|
408 graph.render(data);
|
pankso@282
|
409 } else {
|
pankso@282
|
410 var doc = docFromHTML(htmlText);
|
pankso@282
|
411 var nodes = doc.querySelector(containerSelector).children;
|
pankso@282
|
412 var curClass = 'c' + Date.now();
|
pankso@282
|
413 while (nodes.length) {
|
pankso@282
|
414 var node = nodes[0];
|
pankso@282
|
415 node = document.adoptNode(node);
|
pankso@282
|
416 node.classList.add(curClass);
|
pankso@282
|
417 container.appendChild(node);
|
pankso@282
|
418 }
|
pankso@282
|
419 process_dates('.' + curClass);
|
pankso@282
|
420 }
|
pankso@282
|
421
|
pankso@282
|
422 nextPageVar = nextPageVarGet(htmlText, nextPageVar);
|
pankso@282
|
423 },
|
pankso@282
|
424 function onerror(errorText) {
|
pankso@282
|
425 var message = {
|
pankso@282
|
426 class: 'scroll-loading-error',
|
pankso@282
|
427 text: 'Error: ' + errorText
|
pankso@282
|
428 };
|
pankso@282
|
429 appendFormatHTML(container, messageFormat, message);
|
pankso@282
|
430 },
|
pankso@282
|
431 function oncomplete() {
|
pankso@282
|
432 removeByClassName('scroll-loading');
|
pankso@282
|
433 updateInitiated = false;
|
pankso@282
|
434 scrollHandler();
|
pankso@282
|
435 }
|
pankso@282
|
436 );
|
pankso@282
|
437 }
|
pankso@282
|
438 }
|
pankso@282
|
439
|
pankso@282
|
440 window.addEventListener('scroll', scrollHandler);
|
pankso@282
|
441 window.addEventListener('resize', scrollHandler);
|
pankso@282
|
442 scrollHandler();
|
pankso@282
|
443 }
|