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