Fossil SCM
Final chunk load mode implemented, but diff table widths have somehow been broken (too wide - not sure since when). This version is suitable for playing with and feedback, but not quite yet for trunk.
Commit
5938083e4878dd77fa20999e8d3731809f5f4e1a552fbb7c6ee1bfa53e5ad215
Parent
a7fbefee38a1c52…
1 file changed
+120
-56
+120
-56
| --- src/fossil.diff.js | ||
| +++ src/fossil.diff.js | ||
| @@ -215,11 +215,11 @@ | ||
| 215 | 215 | The goal is to base these controls roughly on github's, a good |
| 216 | 216 | example of which, for use as a model, is: |
| 217 | 217 | |
| 218 | 218 | https://github.com/msteveb/autosetup/commit/235925e914a52a542 |
| 219 | 219 | */ |
| 220 | - Diff.ChunkLoadControls = function(tr){ | |
| 220 | + const ChunkLoadControls = function(tr){ | |
| 221 | 221 | this.e = {/*DOM elements*/ |
| 222 | 222 | tr: tr, |
| 223 | 223 | table: tr.parentElement/*TBODY*/.parentElement |
| 224 | 224 | }; |
| 225 | 225 | this.isSplit = this.e.table.classList.contains('splitdiff')/*else udiff*/; |
| @@ -275,18 +275,18 @@ | ||
| 275 | 275 | && ((this.pos.next.startLhs - this.pos.prev.endLhs) |
| 276 | 276 | <= Diff.config.chunkLoadLines))){ |
| 277 | 277 | /* Place a single button to load the whole block, rather |
| 278 | 278 | than separate up/down buttons. */ |
| 279 | 279 | btnDown = false; |
| 280 | - btnUp = this.createButton(0); | |
| 280 | + btnUp = this.createButton(this.FetchType.FillGap); | |
| 281 | 281 | }else{ |
| 282 | 282 | /* Figure out which chunk-load buttons to add... */ |
| 283 | 283 | if(this.pos.prev){ |
| 284 | - btnDown = this.createButton(1); | |
| 284 | + btnDown = this.createButton(this.FetchType.PrevDown); | |
| 285 | 285 | } |
| 286 | 286 | if(this.pos.next){ |
| 287 | - btnUp = this.createButton(-1); | |
| 287 | + btnUp = this.createButton(this.FetchType.NextUp); | |
| 288 | 288 | } |
| 289 | 289 | } |
| 290 | 290 | //this.e.btnUp = btnUp; |
| 291 | 291 | //this.e.btnDown = btnDown; |
| 292 | 292 | if(btnDown) D.append(this.e.btnWrapper, btnDown); |
| @@ -295,47 +295,61 @@ | ||
| 295 | 295 | this.e.posState = D.span(); |
| 296 | 296 | D.append(this.e.btnWrapper, this.e.posState); |
| 297 | 297 | this.updatePosDebug(); |
| 298 | 298 | }; |
| 299 | 299 | |
| 300 | - Diff.ChunkLoadControls.prototype = { | |
| 300 | + ChunkLoadControls.prototype = { | |
| 301 | + /** An "enum" of values describing the types of context | |
| 302 | + fetches/operations performed by this type. The values in this | |
| 303 | + object must not be changed without modifying all logic which | |
| 304 | + relies on their relative order. */ | |
| 305 | + FetchType:{ | |
| 306 | + /** Append context to the bottom of the previous diff chunk. */ | |
| 307 | + PrevDown: 1, | |
| 308 | + /** Fill a complete gap between the previous/next diff chunks | |
| 309 | + or at the start of the next chunk or end of the previous | |
| 310 | + chunks. */ | |
| 311 | + FillGap: 0, | |
| 312 | + /** Prepend context to the start of the next diff chunk. */ | |
| 313 | + NextUp: -1 | |
| 314 | + }, | |
| 301 | 315 | config: { |
| 302 | 316 | /* |
| 303 | 317 | glyphUp: '⇡', //'&#uarr;', |
| 304 | 318 | glyphDown: '⇣' //'&#darr;' |
| 305 | 319 | */ |
| 306 | 320 | }, |
| 307 | 321 | |
| 308 | 322 | /** |
| 309 | 323 | Creates and returns a button element for fetching a chunk in |
| 310 | - the given direction (as documented for fetchChunk()). | |
| 324 | + the given fetchType (as documented for fetchChunk()). | |
| 311 | 325 | */ |
| 312 | - createButton: function(direction){ | |
| 326 | + createButton: function(fetchType){ | |
| 313 | 327 | let b; |
| 314 | - switch(direction){ | |
| 315 | - case 1: | |
| 328 | + switch(fetchType){ | |
| 329 | + case this.FetchType.PrevDown: | |
| 316 | 330 | b = D.append( |
| 317 | 331 | D.addClass(D.span(), 'button', 'down'), |
| 318 | 332 | D.span(/*glyph holder*/) |
| 319 | 333 | ); |
| 320 | 334 | break; |
| 321 | - case 0: | |
| 335 | + case this.FetchType.FillGap: | |
| 322 | 336 | b = D.append( |
| 323 | 337 | D.addClass(D.span(), 'button', 'up', 'down'), |
| 324 | 338 | D.span(/*glyph holder*/) |
| 325 | 339 | ); |
| 326 | 340 | break; |
| 327 | - case -1: | |
| 341 | + case this.FetchType.NextUp: | |
| 328 | 342 | b = D.append( |
| 329 | 343 | D.addClass(D.span(), 'button', 'up'), |
| 330 | 344 | D.span(/*glyph holder*/) |
| 331 | 345 | ); |
| 332 | 346 | break; |
| 333 | 347 | default: |
| 334 | - throw new Error("Internal API misuse: unexpected direction value "+direction); | |
| 348 | + throw new Error("Internal API misuse: unexpected fetchType value "+fetchType); | |
| 335 | 349 | } |
| 336 | - b.addEventListener('click', ()=>this.fetchChunk(direction),false); | |
| 350 | + b.addEventListener('click', ()=>this.fetchChunk(fetchType),false); | |
| 337 | 351 | return b; |
| 338 | 352 | }, |
| 339 | 353 | |
| 340 | 354 | updatePosDebug: function(){ |
| 341 | 355 | if(this.e.posState){ |
| @@ -353,43 +367,55 @@ | ||
| 353 | 367 | delete this.e.tr; |
| 354 | 368 | delete this.e; |
| 355 | 369 | delete this.pos; |
| 356 | 370 | }, |
| 357 | 371 | |
| 358 | - injectResponse: function f(direction/*as for fetchChunk()*/, | |
| 372 | + /** | |
| 373 | + If the gap between this.pos.endLhs/startLhs is less than or equal to | |
| 374 | + Diff.config.chunkLoadLines then this function replaces any up/down buttons | |
| 375 | + with a gap-filler button, else it's a no-op. Returns this object. | |
| 376 | + */ | |
| 377 | + maybeReplaceButtons: function(){ | |
| 378 | + if(this.pos.endLhs - this.pos.startLhs <= Diff.config.chunkLoadLines){ | |
| 379 | + D.clearElement(this.e.btnWrapper); | |
| 380 | + D.append(this.e.btnWrapper, this.createButton(this.FetchType.FillGap)); | |
| 381 | + } | |
| 382 | + return this; | |
| 383 | + }, | |
| 384 | + | |
| 385 | + /** | |
| 386 | + Callack for /jchunk responses. | |
| 387 | + */ | |
| 388 | + injectResponse: function f(fetchType/*as for fetchChunk()*/, | |
| 359 | 389 | urlParam/*from fetchChunk()*/, |
| 360 | 390 | lines/*response lines*/){ |
| 361 | 391 | if(!lines.length){ |
| 362 | 392 | /* No more data to load */ |
| 363 | 393 | this.destroy(); |
| 364 | 394 | return this; |
| 365 | 395 | } |
| 366 | - console.debug("Loading line range ",urlParam.from,"-",urlParam.to); | |
| 396 | + console.debug("Loaded line range ",urlParam.from,"-",urlParam.to, "fetchType ",fetchType); | |
| 367 | 397 | const lineno = [], |
| 368 | 398 | trPrev = this.e.tr.previousElementSibling, |
| 369 | 399 | trNext = this.e.tr.nextElementSibling, |
| 370 | - doAppend = (!!trPrev && direction>=0) /* true to append to previous TR, else prepend to NEXT TR */; | |
| 371 | - const tr = trPrev || trNext; | |
| 372 | - if(direction<0){ | |
| 373 | - console.error("this case is not yet handled",arguments); | |
| 374 | - return this; | |
| 375 | - } | |
| 400 | + doAppend = ( | |
| 401 | + !!trPrev && fetchType>=this.FetchType.FillGap | |
| 402 | + ) /* true to append to previous TR, else prepend to NEXT TR */; | |
| 403 | + const tr = doAppend ? trPrev : trNext; | |
| 376 | 404 | const joinTr = ( |
| 377 | - 0===direction && trPrev && trNext | |
| 405 | + this.FetchType.FillGap===fetchType && trPrev && trNext | |
| 378 | 406 | ) ? trNext : false |
| 379 | 407 | /* Truthy if we want to combine trPrev, the new content, and |
| 380 | - trNext into trPrev and remove trNext. */; | |
| 408 | + trNext into trPrev and then remove trNext. */; | |
| 381 | 409 | let i, td; |
| 382 | - | |
| 383 | 410 | if(!f.convertLines){ |
| 384 | 411 | f.convertLines = function(li){ |
| 385 | 412 | return li.join('\n') |
| 386 | 413 | .replaceAll('&','&') |
| 387 | 414 | .replaceAll('<','<')+'\n'; |
| 388 | 415 | }; |
| 389 | 416 | } |
| 390 | - | |
| 391 | 417 | if(1){ // LHS line numbers... |
| 392 | 418 | const selector = '.difflnl > pre'; |
| 393 | 419 | td = tr.querySelector(selector); |
| 394 | 420 | const lnTo = Math.min(urlParam.to, |
| 395 | 421 | urlParam.from + |
| @@ -435,11 +461,11 @@ | ||
| 435 | 461 | content.push(trNext.querySelector(selector).innerHTML); |
| 436 | 462 | } |
| 437 | 463 | td.innerHTML = content.join(''); |
| 438 | 464 | } |
| 439 | 465 | |
| 440 | - if(0===direction){ | |
| 466 | + if(this.FetchType.FillGap===fetchType){ | |
| 441 | 467 | /* Closing the whole gap between two chunks or a whole gap |
| 442 | 468 | at the start or end of a diff. */ |
| 443 | 469 | // RHS line numbers... |
| 444 | 470 | let startLnR = this.pos.prev |
| 445 | 471 | ? this.pos.prev.endRhs+1 /* Closing the whole gap between two chunks |
| @@ -462,20 +488,18 @@ | ||
| 462 | 488 | td.innerHTML = content.join(''); |
| 463 | 489 | if(joinTr) D.remove(joinTr); |
| 464 | 490 | Diff.checkTableWidth(true); |
| 465 | 491 | this.destroy(); |
| 466 | 492 | return this; |
| 467 | - }else if(1===direction){ | |
| 468 | - /* Expanding previous TR downwards. */ | |
| 493 | + }else if(this.FetchType.PrevDown===fetchType){ | |
| 494 | + /* Append context to previous TR. */ | |
| 469 | 495 | // RHS line numbers... |
| 470 | 496 | let startLnR = this.pos.prev.endRhs+1; |
| 471 | 497 | lineno.length = lines.length; |
| 472 | 498 | for( i = startLnR; i < startLnR + lines.length; ++i ){ |
| 473 | 499 | lineno[i-startLnR] = i; |
| 474 | 500 | } |
| 475 | - console.debug("lineno.length =",lineno.length); | |
| 476 | - console.debug("lines.length =",lineno.length); | |
| 477 | 501 | this.pos.startLhs += lines.length; |
| 478 | 502 | this.pos.prev.endRhs += lines.length; |
| 479 | 503 | this.pos.prev.endLhs += lines.length; |
| 480 | 504 | const selector = '.difflnr > pre'; |
| 481 | 505 | td = tr.querySelector(selector); |
| @@ -487,102 +511,141 @@ | ||
| 487 | 511 | td.innerHTML = content.join(''); |
| 488 | 512 | if(lines.length < (urlParam.to - urlParam.from)){ |
| 489 | 513 | /* No more data. */ |
| 490 | 514 | this.destroy(); |
| 491 | 515 | }else{ |
| 516 | + this.maybeReplaceButtons(); | |
| 517 | + this.updatePosDebug(); | |
| 518 | + } | |
| 519 | + Diff.checkTableWidth(true); | |
| 520 | + return this; | |
| 521 | + }else if(this.FetchType.NextUp===fetchType){ | |
| 522 | + /* Prepend content to next TR. */ | |
| 523 | + // RHS line numbers... | |
| 524 | + if(doAppend){ | |
| 525 | + throw new Error("Internal precondition violation: doAppend is true."); | |
| 526 | + } | |
| 527 | + let startLnR = this.pos.next.startRhs - lines.length; | |
| 528 | + lineno.length = lines.length; | |
| 529 | + for( i = startLnR; i < startLnR + lines.length; ++i ){ | |
| 530 | + lineno[i-startLnR] = i; | |
| 531 | + } | |
| 532 | + this.pos.endLhs -= lines.length; | |
| 533 | + this.pos.next.startRhs -= lines.length; | |
| 534 | + this.pos.next.startLhs -= lines.length; | |
| 535 | + const selector = '.difflnr > pre'; | |
| 536 | + td = tr.querySelector(selector); | |
| 537 | + const lineNoTxt = lineno.join('\n')+'\n'; | |
| 538 | + lineno.length = 0; | |
| 539 | + td.innerHTML = lineNoTxt + td.innerHTML; | |
| 540 | + if(this.pos.endLhs<=1 | |
| 541 | + || lines.length < (urlParam.to - urlParam.from)){ | |
| 542 | + /* No more data. */ | |
| 543 | + this.destroy(); | |
| 544 | + }else{ | |
| 545 | + this.maybeReplaceButtons(); | |
| 492 | 546 | this.updatePosDebug(); |
| 493 | 547 | } |
| 494 | 548 | Diff.checkTableWidth(true); |
| 495 | 549 | return this; |
| 496 | 550 | }else{ |
| 497 | - console.debug("TODO: handle load of partial next/prev"); | |
| 498 | - this.updatePosDebug(); | |
| 551 | + throw new Error("Unexpected 'fetchType' value."); | |
| 499 | 552 | } |
| 500 | 553 | }, |
| 501 | 554 | |
| 502 | 555 | /** |
| 503 | - Fetches and inserts a line chunk. direction is: | |
| 504 | - | |
| 505 | - -1 = upwards from next chunk (this.pos.next) | |
| 506 | - | |
| 507 | - 0 = the whole gap between this.pos.prev and this.pos.next, or | |
| 508 | - the whole gap before/after the initial/final chunk in the diff. | |
| 509 | - | |
| 510 | - 1 = downwards from the previous chunk (this.pos.prev) | |
| 556 | + Fetches and inserts a line chunk. fetchType is: | |
| 557 | + | |
| 558 | + this.FetchType.NextUp = upwards from next chunk (this.pos.next) | |
| 559 | + | |
| 560 | + this.FetchType.FillGap = the whole gap between this.pos.prev | |
| 561 | + and this.pos.next, or the whole gap before/after the | |
| 562 | + initial/final chunk in the diff. | |
| 563 | + | |
| 564 | + this.FetchType.PrevDown = downwards from the previous chunk | |
| 565 | + (this.pos.prev) | |
| 511 | 566 | |
| 512 | 567 | Those values are set at the time this object is initialized but |
| 513 | 568 | one instance of this class may have 2 buttons, one each for |
| 514 | - directions -1 and 1. | |
| 569 | + fetchTypes NextUp and PrevDown. | |
| 515 | 570 | |
| 516 | 571 | This is an async operation. While it is in transit, any calls |
| 517 | 572 | to this function will have no effect except (possibly) to emit |
| 518 | 573 | a warning. Returns this object. |
| 519 | 574 | */ |
| 520 | - fetchChunk: function(direction){ | |
| 575 | + fetchChunk: function(fetchType){ | |
| 521 | 576 | /* Forewarning, this is a bit confusing: when fetching the |
| 522 | 577 | previous lines, we're doing so on behalf of the *next* diff |
| 523 | 578 | chunk (this.pos.next), and vice versa. */ |
| 524 | 579 | if(this.$isFetching){ |
| 525 | 580 | F.toast.warning("Cannot load chunk while a load is pending."); |
| 526 | 581 | return this; |
| 527 | 582 | } |
| 528 | - if(direction<0 && !this.pos.next | |
| 529 | - || direction>0 && !this.pos.prev){ | |
| 583 | + if(fetchType===this.FetchType.NextUp && !this.pos.next | |
| 584 | + || fetchType===this.FetchType.PrevDown && !this.pos.prev){ | |
| 530 | 585 | console.error("Attempt to fetch diff lines but don't have any."); |
| 531 | 586 | return this; |
| 532 | 587 | } |
| 533 | 588 | const fOpt = { |
| 534 | 589 | urlParams:{ |
| 535 | 590 | name: this.fileHash, from: 0, to: 0 |
| 536 | 591 | }, |
| 537 | 592 | aftersend: ()=>delete this.$isFetching, |
| 538 | - onload: (list)=>this.injectResponse(direction,up,list) | |
| 593 | + onload: (list)=>this.injectResponse(fetchType,up,list) | |
| 539 | 594 | }; |
| 540 | 595 | const up = fOpt.urlParams; |
| 541 | - if(direction===0){ | |
| 596 | + if(fetchType===this.FetchType.FillGap){ | |
| 542 | 597 | /* Easiest case: filling a whole gap. */ |
| 543 | 598 | up.from = this.pos.startLhs; |
| 544 | 599 | up.to = this.pos.endLhs; |
| 545 | - }else if(1===direction){ | |
| 546 | - /* Expand previous TR downwards. */ | |
| 600 | + }else if(this.FetchType.PrevDown===fetchType){ | |
| 601 | + /* Append to previous TR. */ | |
| 547 | 602 | if(!this.pos.prev){ |
| 548 | 603 | console.error("Attempt to fetch next diff lines but don't have any."); |
| 549 | 604 | return this; |
| 550 | 605 | } |
| 551 | 606 | up.from = this.pos.prev.endLhs + 1; |
| 552 | 607 | up.to = up.from + |
| 553 | 608 | Diff.config.chunkLoadLines - 1/*b/c request range is inclusive*/; |
| 554 | 609 | if( this.pos.next && this.pos.next.startLhs <= up.to ){ |
| 555 | 610 | up.to = this.pos.next.startLhs - 1; |
| 556 | - direction = 0; | |
| 611 | + fetchType = this.FetchType.FillGap; | |
| 557 | 612 | } |
| 558 | 613 | }else{ |
| 559 | - /* Expand next TR upwards */ | |
| 560 | - console.debug("fetchChunk(",direction,")",up); | |
| 561 | - return this; | |
| 614 | + /* Prepend to next TR */ | |
| 615 | + if(!this.pos.next){ | |
| 616 | + console.error("Attempt to fetch previous diff lines but don't have any."); | |
| 617 | + return this; | |
| 618 | + } | |
| 619 | + up.to = this.pos.next.startLhs - 1; | |
| 620 | + up.from = Math.max(1, up.to - Diff.config.chunkLoadLines + 1); | |
| 621 | + if( this.pos.prev && this.pos.prev.endLhs >= up.from ){ | |
| 622 | + up.from = this.pos.prev.endLhs + 1; | |
| 623 | + fetchType = this.FetchType.FillGap; | |
| 624 | + } | |
| 562 | 625 | } |
| 563 | 626 | this.$isFetching = true; |
| 564 | - console.debug("fetchChunk(",direction,")",up); | |
| 627 | + console.debug("fetchChunk(",fetchType,")",up); | |
| 565 | 628 | Diff.fetchArtifactChunk(fOpt); |
| 566 | 629 | return this; |
| 567 | 630 | } |
| 568 | 631 | }; |
| 569 | 632 | |
| 570 | - Diff.addDiffSkipHandlers = function(){ | |
| 633 | + const addDiffSkipHandlers = function(){ | |
| 571 | 634 | const tables = document.querySelectorAll('table.diff[data-lefthash]:not(.diffskipped)'); |
| 572 | 635 | /* Potential performance-related TODO: instead of installing all |
| 573 | 636 | of these at once, install them as the corresponding TR is |
| 574 | 637 | scrolled into view. */ |
| 575 | 638 | tables.forEach(function(table){ |
| 576 | 639 | D.addClass(table, 'diffskipped'/*avoid processing these more than once */); |
| 577 | 640 | table.querySelectorAll('tr.diffskip[data-startln]').forEach(function(tr){ |
| 578 | - new Diff.ChunkLoadControls(D.addClass(tr, 'jchunk')); | |
| 641 | + new ChunkLoadControls(D.addClass(tr, 'jchunk')); | |
| 579 | 642 | }); |
| 580 | 643 | }); |
| 581 | 644 | return F; |
| 582 | 645 | }; |
| 583 | - Diff.addDiffSkipHandlers(); | |
| 646 | + addDiffSkipHandlers(); | |
| 584 | 647 | }); |
| 585 | 648 | |
| 586 | 649 | /* Refinements to the display of unified and side-by-side diffs. |
| 587 | 650 | ** |
| 588 | 651 | ** In all cases, the table columns tagged with "difftxt" are expanded, |
| @@ -619,10 +682,11 @@ | ||
| 619 | 682 | if(!f.allDiffs){ |
| 620 | 683 | f.allDiffs = document.querySelectorAll('table.diff'); |
| 621 | 684 | } |
| 622 | 685 | w = f.lastWidth; |
| 623 | 686 | f.allDiffs.forEach((e)=>e.style.maxWidth = w + "px"); |
| 687 | + //console.debug("checkTableWidth(",force,") f.lastWidth =",f.lastWidth); | |
| 624 | 688 | return this; |
| 625 | 689 | }; |
| 626 | 690 | |
| 627 | 691 | const scrollLeft = function(event){ |
| 628 | 692 | //console.debug("scrollLeft",this,event); |
| 629 | 693 |
| --- src/fossil.diff.js | |
| +++ src/fossil.diff.js | |
| @@ -215,11 +215,11 @@ | |
| 215 | The goal is to base these controls roughly on github's, a good |
| 216 | example of which, for use as a model, is: |
| 217 | |
| 218 | https://github.com/msteveb/autosetup/commit/235925e914a52a542 |
| 219 | */ |
| 220 | Diff.ChunkLoadControls = function(tr){ |
| 221 | this.e = {/*DOM elements*/ |
| 222 | tr: tr, |
| 223 | table: tr.parentElement/*TBODY*/.parentElement |
| 224 | }; |
| 225 | this.isSplit = this.e.table.classList.contains('splitdiff')/*else udiff*/; |
| @@ -275,18 +275,18 @@ | |
| 275 | && ((this.pos.next.startLhs - this.pos.prev.endLhs) |
| 276 | <= Diff.config.chunkLoadLines))){ |
| 277 | /* Place a single button to load the whole block, rather |
| 278 | than separate up/down buttons. */ |
| 279 | btnDown = false; |
| 280 | btnUp = this.createButton(0); |
| 281 | }else{ |
| 282 | /* Figure out which chunk-load buttons to add... */ |
| 283 | if(this.pos.prev){ |
| 284 | btnDown = this.createButton(1); |
| 285 | } |
| 286 | if(this.pos.next){ |
| 287 | btnUp = this.createButton(-1); |
| 288 | } |
| 289 | } |
| 290 | //this.e.btnUp = btnUp; |
| 291 | //this.e.btnDown = btnDown; |
| 292 | if(btnDown) D.append(this.e.btnWrapper, btnDown); |
| @@ -295,47 +295,61 @@ | |
| 295 | this.e.posState = D.span(); |
| 296 | D.append(this.e.btnWrapper, this.e.posState); |
| 297 | this.updatePosDebug(); |
| 298 | }; |
| 299 | |
| 300 | Diff.ChunkLoadControls.prototype = { |
| 301 | config: { |
| 302 | /* |
| 303 | glyphUp: '⇡', //'&#uarr;', |
| 304 | glyphDown: '⇣' //'&#darr;' |
| 305 | */ |
| 306 | }, |
| 307 | |
| 308 | /** |
| 309 | Creates and returns a button element for fetching a chunk in |
| 310 | the given direction (as documented for fetchChunk()). |
| 311 | */ |
| 312 | createButton: function(direction){ |
| 313 | let b; |
| 314 | switch(direction){ |
| 315 | case 1: |
| 316 | b = D.append( |
| 317 | D.addClass(D.span(), 'button', 'down'), |
| 318 | D.span(/*glyph holder*/) |
| 319 | ); |
| 320 | break; |
| 321 | case 0: |
| 322 | b = D.append( |
| 323 | D.addClass(D.span(), 'button', 'up', 'down'), |
| 324 | D.span(/*glyph holder*/) |
| 325 | ); |
| 326 | break; |
| 327 | case -1: |
| 328 | b = D.append( |
| 329 | D.addClass(D.span(), 'button', 'up'), |
| 330 | D.span(/*glyph holder*/) |
| 331 | ); |
| 332 | break; |
| 333 | default: |
| 334 | throw new Error("Internal API misuse: unexpected direction value "+direction); |
| 335 | } |
| 336 | b.addEventListener('click', ()=>this.fetchChunk(direction),false); |
| 337 | return b; |
| 338 | }, |
| 339 | |
| 340 | updatePosDebug: function(){ |
| 341 | if(this.e.posState){ |
| @@ -353,43 +367,55 @@ | |
| 353 | delete this.e.tr; |
| 354 | delete this.e; |
| 355 | delete this.pos; |
| 356 | }, |
| 357 | |
| 358 | injectResponse: function f(direction/*as for fetchChunk()*/, |
| 359 | urlParam/*from fetchChunk()*/, |
| 360 | lines/*response lines*/){ |
| 361 | if(!lines.length){ |
| 362 | /* No more data to load */ |
| 363 | this.destroy(); |
| 364 | return this; |
| 365 | } |
| 366 | console.debug("Loading line range ",urlParam.from,"-",urlParam.to); |
| 367 | const lineno = [], |
| 368 | trPrev = this.e.tr.previousElementSibling, |
| 369 | trNext = this.e.tr.nextElementSibling, |
| 370 | doAppend = (!!trPrev && direction>=0) /* true to append to previous TR, else prepend to NEXT TR */; |
| 371 | const tr = trPrev || trNext; |
| 372 | if(direction<0){ |
| 373 | console.error("this case is not yet handled",arguments); |
| 374 | return this; |
| 375 | } |
| 376 | const joinTr = ( |
| 377 | 0===direction && trPrev && trNext |
| 378 | ) ? trNext : false |
| 379 | /* Truthy if we want to combine trPrev, the new content, and |
| 380 | trNext into trPrev and remove trNext. */; |
| 381 | let i, td; |
| 382 | |
| 383 | if(!f.convertLines){ |
| 384 | f.convertLines = function(li){ |
| 385 | return li.join('\n') |
| 386 | .replaceAll('&','&') |
| 387 | .replaceAll('<','<')+'\n'; |
| 388 | }; |
| 389 | } |
| 390 | |
| 391 | if(1){ // LHS line numbers... |
| 392 | const selector = '.difflnl > pre'; |
| 393 | td = tr.querySelector(selector); |
| 394 | const lnTo = Math.min(urlParam.to, |
| 395 | urlParam.from + |
| @@ -435,11 +461,11 @@ | |
| 435 | content.push(trNext.querySelector(selector).innerHTML); |
| 436 | } |
| 437 | td.innerHTML = content.join(''); |
| 438 | } |
| 439 | |
| 440 | if(0===direction){ |
| 441 | /* Closing the whole gap between two chunks or a whole gap |
| 442 | at the start or end of a diff. */ |
| 443 | // RHS line numbers... |
| 444 | let startLnR = this.pos.prev |
| 445 | ? this.pos.prev.endRhs+1 /* Closing the whole gap between two chunks |
| @@ -462,20 +488,18 @@ | |
| 462 | td.innerHTML = content.join(''); |
| 463 | if(joinTr) D.remove(joinTr); |
| 464 | Diff.checkTableWidth(true); |
| 465 | this.destroy(); |
| 466 | return this; |
| 467 | }else if(1===direction){ |
| 468 | /* Expanding previous TR downwards. */ |
| 469 | // RHS line numbers... |
| 470 | let startLnR = this.pos.prev.endRhs+1; |
| 471 | lineno.length = lines.length; |
| 472 | for( i = startLnR; i < startLnR + lines.length; ++i ){ |
| 473 | lineno[i-startLnR] = i; |
| 474 | } |
| 475 | console.debug("lineno.length =",lineno.length); |
| 476 | console.debug("lines.length =",lineno.length); |
| 477 | this.pos.startLhs += lines.length; |
| 478 | this.pos.prev.endRhs += lines.length; |
| 479 | this.pos.prev.endLhs += lines.length; |
| 480 | const selector = '.difflnr > pre'; |
| 481 | td = tr.querySelector(selector); |
| @@ -487,102 +511,141 @@ | |
| 487 | td.innerHTML = content.join(''); |
| 488 | if(lines.length < (urlParam.to - urlParam.from)){ |
| 489 | /* No more data. */ |
| 490 | this.destroy(); |
| 491 | }else{ |
| 492 | this.updatePosDebug(); |
| 493 | } |
| 494 | Diff.checkTableWidth(true); |
| 495 | return this; |
| 496 | }else{ |
| 497 | console.debug("TODO: handle load of partial next/prev"); |
| 498 | this.updatePosDebug(); |
| 499 | } |
| 500 | }, |
| 501 | |
| 502 | /** |
| 503 | Fetches and inserts a line chunk. direction is: |
| 504 | |
| 505 | -1 = upwards from next chunk (this.pos.next) |
| 506 | |
| 507 | 0 = the whole gap between this.pos.prev and this.pos.next, or |
| 508 | the whole gap before/after the initial/final chunk in the diff. |
| 509 | |
| 510 | 1 = downwards from the previous chunk (this.pos.prev) |
| 511 | |
| 512 | Those values are set at the time this object is initialized but |
| 513 | one instance of this class may have 2 buttons, one each for |
| 514 | directions -1 and 1. |
| 515 | |
| 516 | This is an async operation. While it is in transit, any calls |
| 517 | to this function will have no effect except (possibly) to emit |
| 518 | a warning. Returns this object. |
| 519 | */ |
| 520 | fetchChunk: function(direction){ |
| 521 | /* Forewarning, this is a bit confusing: when fetching the |
| 522 | previous lines, we're doing so on behalf of the *next* diff |
| 523 | chunk (this.pos.next), and vice versa. */ |
| 524 | if(this.$isFetching){ |
| 525 | F.toast.warning("Cannot load chunk while a load is pending."); |
| 526 | return this; |
| 527 | } |
| 528 | if(direction<0 && !this.pos.next |
| 529 | || direction>0 && !this.pos.prev){ |
| 530 | console.error("Attempt to fetch diff lines but don't have any."); |
| 531 | return this; |
| 532 | } |
| 533 | const fOpt = { |
| 534 | urlParams:{ |
| 535 | name: this.fileHash, from: 0, to: 0 |
| 536 | }, |
| 537 | aftersend: ()=>delete this.$isFetching, |
| 538 | onload: (list)=>this.injectResponse(direction,up,list) |
| 539 | }; |
| 540 | const up = fOpt.urlParams; |
| 541 | if(direction===0){ |
| 542 | /* Easiest case: filling a whole gap. */ |
| 543 | up.from = this.pos.startLhs; |
| 544 | up.to = this.pos.endLhs; |
| 545 | }else if(1===direction){ |
| 546 | /* Expand previous TR downwards. */ |
| 547 | if(!this.pos.prev){ |
| 548 | console.error("Attempt to fetch next diff lines but don't have any."); |
| 549 | return this; |
| 550 | } |
| 551 | up.from = this.pos.prev.endLhs + 1; |
| 552 | up.to = up.from + |
| 553 | Diff.config.chunkLoadLines - 1/*b/c request range is inclusive*/; |
| 554 | if( this.pos.next && this.pos.next.startLhs <= up.to ){ |
| 555 | up.to = this.pos.next.startLhs - 1; |
| 556 | direction = 0; |
| 557 | } |
| 558 | }else{ |
| 559 | /* Expand next TR upwards */ |
| 560 | console.debug("fetchChunk(",direction,")",up); |
| 561 | return this; |
| 562 | } |
| 563 | this.$isFetching = true; |
| 564 | console.debug("fetchChunk(",direction,")",up); |
| 565 | Diff.fetchArtifactChunk(fOpt); |
| 566 | return this; |
| 567 | } |
| 568 | }; |
| 569 | |
| 570 | Diff.addDiffSkipHandlers = function(){ |
| 571 | const tables = document.querySelectorAll('table.diff[data-lefthash]:not(.diffskipped)'); |
| 572 | /* Potential performance-related TODO: instead of installing all |
| 573 | of these at once, install them as the corresponding TR is |
| 574 | scrolled into view. */ |
| 575 | tables.forEach(function(table){ |
| 576 | D.addClass(table, 'diffskipped'/*avoid processing these more than once */); |
| 577 | table.querySelectorAll('tr.diffskip[data-startln]').forEach(function(tr){ |
| 578 | new Diff.ChunkLoadControls(D.addClass(tr, 'jchunk')); |
| 579 | }); |
| 580 | }); |
| 581 | return F; |
| 582 | }; |
| 583 | Diff.addDiffSkipHandlers(); |
| 584 | }); |
| 585 | |
| 586 | /* Refinements to the display of unified and side-by-side diffs. |
| 587 | ** |
| 588 | ** In all cases, the table columns tagged with "difftxt" are expanded, |
| @@ -619,10 +682,11 @@ | |
| 619 | if(!f.allDiffs){ |
| 620 | f.allDiffs = document.querySelectorAll('table.diff'); |
| 621 | } |
| 622 | w = f.lastWidth; |
| 623 | f.allDiffs.forEach((e)=>e.style.maxWidth = w + "px"); |
| 624 | return this; |
| 625 | }; |
| 626 | |
| 627 | const scrollLeft = function(event){ |
| 628 | //console.debug("scrollLeft",this,event); |
| 629 |
| --- src/fossil.diff.js | |
| +++ src/fossil.diff.js | |
| @@ -215,11 +215,11 @@ | |
| 215 | The goal is to base these controls roughly on github's, a good |
| 216 | example of which, for use as a model, is: |
| 217 | |
| 218 | https://github.com/msteveb/autosetup/commit/235925e914a52a542 |
| 219 | */ |
| 220 | const ChunkLoadControls = function(tr){ |
| 221 | this.e = {/*DOM elements*/ |
| 222 | tr: tr, |
| 223 | table: tr.parentElement/*TBODY*/.parentElement |
| 224 | }; |
| 225 | this.isSplit = this.e.table.classList.contains('splitdiff')/*else udiff*/; |
| @@ -275,18 +275,18 @@ | |
| 275 | && ((this.pos.next.startLhs - this.pos.prev.endLhs) |
| 276 | <= Diff.config.chunkLoadLines))){ |
| 277 | /* Place a single button to load the whole block, rather |
| 278 | than separate up/down buttons. */ |
| 279 | btnDown = false; |
| 280 | btnUp = this.createButton(this.FetchType.FillGap); |
| 281 | }else{ |
| 282 | /* Figure out which chunk-load buttons to add... */ |
| 283 | if(this.pos.prev){ |
| 284 | btnDown = this.createButton(this.FetchType.PrevDown); |
| 285 | } |
| 286 | if(this.pos.next){ |
| 287 | btnUp = this.createButton(this.FetchType.NextUp); |
| 288 | } |
| 289 | } |
| 290 | //this.e.btnUp = btnUp; |
| 291 | //this.e.btnDown = btnDown; |
| 292 | if(btnDown) D.append(this.e.btnWrapper, btnDown); |
| @@ -295,47 +295,61 @@ | |
| 295 | this.e.posState = D.span(); |
| 296 | D.append(this.e.btnWrapper, this.e.posState); |
| 297 | this.updatePosDebug(); |
| 298 | }; |
| 299 | |
| 300 | ChunkLoadControls.prototype = { |
| 301 | /** An "enum" of values describing the types of context |
| 302 | fetches/operations performed by this type. The values in this |
| 303 | object must not be changed without modifying all logic which |
| 304 | relies on their relative order. */ |
| 305 | FetchType:{ |
| 306 | /** Append context to the bottom of the previous diff chunk. */ |
| 307 | PrevDown: 1, |
| 308 | /** Fill a complete gap between the previous/next diff chunks |
| 309 | or at the start of the next chunk or end of the previous |
| 310 | chunks. */ |
| 311 | FillGap: 0, |
| 312 | /** Prepend context to the start of the next diff chunk. */ |
| 313 | NextUp: -1 |
| 314 | }, |
| 315 | config: { |
| 316 | /* |
| 317 | glyphUp: '⇡', //'&#uarr;', |
| 318 | glyphDown: '⇣' //'&#darr;' |
| 319 | */ |
| 320 | }, |
| 321 | |
| 322 | /** |
| 323 | Creates and returns a button element for fetching a chunk in |
| 324 | the given fetchType (as documented for fetchChunk()). |
| 325 | */ |
| 326 | createButton: function(fetchType){ |
| 327 | let b; |
| 328 | switch(fetchType){ |
| 329 | case this.FetchType.PrevDown: |
| 330 | b = D.append( |
| 331 | D.addClass(D.span(), 'button', 'down'), |
| 332 | D.span(/*glyph holder*/) |
| 333 | ); |
| 334 | break; |
| 335 | case this.FetchType.FillGap: |
| 336 | b = D.append( |
| 337 | D.addClass(D.span(), 'button', 'up', 'down'), |
| 338 | D.span(/*glyph holder*/) |
| 339 | ); |
| 340 | break; |
| 341 | case this.FetchType.NextUp: |
| 342 | b = D.append( |
| 343 | D.addClass(D.span(), 'button', 'up'), |
| 344 | D.span(/*glyph holder*/) |
| 345 | ); |
| 346 | break; |
| 347 | default: |
| 348 | throw new Error("Internal API misuse: unexpected fetchType value "+fetchType); |
| 349 | } |
| 350 | b.addEventListener('click', ()=>this.fetchChunk(fetchType),false); |
| 351 | return b; |
| 352 | }, |
| 353 | |
| 354 | updatePosDebug: function(){ |
| 355 | if(this.e.posState){ |
| @@ -353,43 +367,55 @@ | |
| 367 | delete this.e.tr; |
| 368 | delete this.e; |
| 369 | delete this.pos; |
| 370 | }, |
| 371 | |
| 372 | /** |
| 373 | If the gap between this.pos.endLhs/startLhs is less than or equal to |
| 374 | Diff.config.chunkLoadLines then this function replaces any up/down buttons |
| 375 | with a gap-filler button, else it's a no-op. Returns this object. |
| 376 | */ |
| 377 | maybeReplaceButtons: function(){ |
| 378 | if(this.pos.endLhs - this.pos.startLhs <= Diff.config.chunkLoadLines){ |
| 379 | D.clearElement(this.e.btnWrapper); |
| 380 | D.append(this.e.btnWrapper, this.createButton(this.FetchType.FillGap)); |
| 381 | } |
| 382 | return this; |
| 383 | }, |
| 384 | |
| 385 | /** |
| 386 | Callack for /jchunk responses. |
| 387 | */ |
| 388 | injectResponse: function f(fetchType/*as for fetchChunk()*/, |
| 389 | urlParam/*from fetchChunk()*/, |
| 390 | lines/*response lines*/){ |
| 391 | if(!lines.length){ |
| 392 | /* No more data to load */ |
| 393 | this.destroy(); |
| 394 | return this; |
| 395 | } |
| 396 | console.debug("Loaded line range ",urlParam.from,"-",urlParam.to, "fetchType ",fetchType); |
| 397 | const lineno = [], |
| 398 | trPrev = this.e.tr.previousElementSibling, |
| 399 | trNext = this.e.tr.nextElementSibling, |
| 400 | doAppend = ( |
| 401 | !!trPrev && fetchType>=this.FetchType.FillGap |
| 402 | ) /* true to append to previous TR, else prepend to NEXT TR */; |
| 403 | const tr = doAppend ? trPrev : trNext; |
| 404 | const joinTr = ( |
| 405 | this.FetchType.FillGap===fetchType && trPrev && trNext |
| 406 | ) ? trNext : false |
| 407 | /* Truthy if we want to combine trPrev, the new content, and |
| 408 | trNext into trPrev and then remove trNext. */; |
| 409 | let i, td; |
| 410 | if(!f.convertLines){ |
| 411 | f.convertLines = function(li){ |
| 412 | return li.join('\n') |
| 413 | .replaceAll('&','&') |
| 414 | .replaceAll('<','<')+'\n'; |
| 415 | }; |
| 416 | } |
| 417 | if(1){ // LHS line numbers... |
| 418 | const selector = '.difflnl > pre'; |
| 419 | td = tr.querySelector(selector); |
| 420 | const lnTo = Math.min(urlParam.to, |
| 421 | urlParam.from + |
| @@ -435,11 +461,11 @@ | |
| 461 | content.push(trNext.querySelector(selector).innerHTML); |
| 462 | } |
| 463 | td.innerHTML = content.join(''); |
| 464 | } |
| 465 | |
| 466 | if(this.FetchType.FillGap===fetchType){ |
| 467 | /* Closing the whole gap between two chunks or a whole gap |
| 468 | at the start or end of a diff. */ |
| 469 | // RHS line numbers... |
| 470 | let startLnR = this.pos.prev |
| 471 | ? this.pos.prev.endRhs+1 /* Closing the whole gap between two chunks |
| @@ -462,20 +488,18 @@ | |
| 488 | td.innerHTML = content.join(''); |
| 489 | if(joinTr) D.remove(joinTr); |
| 490 | Diff.checkTableWidth(true); |
| 491 | this.destroy(); |
| 492 | return this; |
| 493 | }else if(this.FetchType.PrevDown===fetchType){ |
| 494 | /* Append context to previous TR. */ |
| 495 | // RHS line numbers... |
| 496 | let startLnR = this.pos.prev.endRhs+1; |
| 497 | lineno.length = lines.length; |
| 498 | for( i = startLnR; i < startLnR + lines.length; ++i ){ |
| 499 | lineno[i-startLnR] = i; |
| 500 | } |
| 501 | this.pos.startLhs += lines.length; |
| 502 | this.pos.prev.endRhs += lines.length; |
| 503 | this.pos.prev.endLhs += lines.length; |
| 504 | const selector = '.difflnr > pre'; |
| 505 | td = tr.querySelector(selector); |
| @@ -487,102 +511,141 @@ | |
| 511 | td.innerHTML = content.join(''); |
| 512 | if(lines.length < (urlParam.to - urlParam.from)){ |
| 513 | /* No more data. */ |
| 514 | this.destroy(); |
| 515 | }else{ |
| 516 | this.maybeReplaceButtons(); |
| 517 | this.updatePosDebug(); |
| 518 | } |
| 519 | Diff.checkTableWidth(true); |
| 520 | return this; |
| 521 | }else if(this.FetchType.NextUp===fetchType){ |
| 522 | /* Prepend content to next TR. */ |
| 523 | // RHS line numbers... |
| 524 | if(doAppend){ |
| 525 | throw new Error("Internal precondition violation: doAppend is true."); |
| 526 | } |
| 527 | let startLnR = this.pos.next.startRhs - lines.length; |
| 528 | lineno.length = lines.length; |
| 529 | for( i = startLnR; i < startLnR + lines.length; ++i ){ |
| 530 | lineno[i-startLnR] = i; |
| 531 | } |
| 532 | this.pos.endLhs -= lines.length; |
| 533 | this.pos.next.startRhs -= lines.length; |
| 534 | this.pos.next.startLhs -= lines.length; |
| 535 | const selector = '.difflnr > pre'; |
| 536 | td = tr.querySelector(selector); |
| 537 | const lineNoTxt = lineno.join('\n')+'\n'; |
| 538 | lineno.length = 0; |
| 539 | td.innerHTML = lineNoTxt + td.innerHTML; |
| 540 | if(this.pos.endLhs<=1 |
| 541 | || lines.length < (urlParam.to - urlParam.from)){ |
| 542 | /* No more data. */ |
| 543 | this.destroy(); |
| 544 | }else{ |
| 545 | this.maybeReplaceButtons(); |
| 546 | this.updatePosDebug(); |
| 547 | } |
| 548 | Diff.checkTableWidth(true); |
| 549 | return this; |
| 550 | }else{ |
| 551 | throw new Error("Unexpected 'fetchType' value."); |
| 552 | } |
| 553 | }, |
| 554 | |
| 555 | /** |
| 556 | Fetches and inserts a line chunk. fetchType is: |
| 557 | |
| 558 | this.FetchType.NextUp = upwards from next chunk (this.pos.next) |
| 559 | |
| 560 | this.FetchType.FillGap = the whole gap between this.pos.prev |
| 561 | and this.pos.next, or the whole gap before/after the |
| 562 | initial/final chunk in the diff. |
| 563 | |
| 564 | this.FetchType.PrevDown = downwards from the previous chunk |
| 565 | (this.pos.prev) |
| 566 | |
| 567 | Those values are set at the time this object is initialized but |
| 568 | one instance of this class may have 2 buttons, one each for |
| 569 | fetchTypes NextUp and PrevDown. |
| 570 | |
| 571 | This is an async operation. While it is in transit, any calls |
| 572 | to this function will have no effect except (possibly) to emit |
| 573 | a warning. Returns this object. |
| 574 | */ |
| 575 | fetchChunk: function(fetchType){ |
| 576 | /* Forewarning, this is a bit confusing: when fetching the |
| 577 | previous lines, we're doing so on behalf of the *next* diff |
| 578 | chunk (this.pos.next), and vice versa. */ |
| 579 | if(this.$isFetching){ |
| 580 | F.toast.warning("Cannot load chunk while a load is pending."); |
| 581 | return this; |
| 582 | } |
| 583 | if(fetchType===this.FetchType.NextUp && !this.pos.next |
| 584 | || fetchType===this.FetchType.PrevDown && !this.pos.prev){ |
| 585 | console.error("Attempt to fetch diff lines but don't have any."); |
| 586 | return this; |
| 587 | } |
| 588 | const fOpt = { |
| 589 | urlParams:{ |
| 590 | name: this.fileHash, from: 0, to: 0 |
| 591 | }, |
| 592 | aftersend: ()=>delete this.$isFetching, |
| 593 | onload: (list)=>this.injectResponse(fetchType,up,list) |
| 594 | }; |
| 595 | const up = fOpt.urlParams; |
| 596 | if(fetchType===this.FetchType.FillGap){ |
| 597 | /* Easiest case: filling a whole gap. */ |
| 598 | up.from = this.pos.startLhs; |
| 599 | up.to = this.pos.endLhs; |
| 600 | }else if(this.FetchType.PrevDown===fetchType){ |
| 601 | /* Append to previous TR. */ |
| 602 | if(!this.pos.prev){ |
| 603 | console.error("Attempt to fetch next diff lines but don't have any."); |
| 604 | return this; |
| 605 | } |
| 606 | up.from = this.pos.prev.endLhs + 1; |
| 607 | up.to = up.from + |
| 608 | Diff.config.chunkLoadLines - 1/*b/c request range is inclusive*/; |
| 609 | if( this.pos.next && this.pos.next.startLhs <= up.to ){ |
| 610 | up.to = this.pos.next.startLhs - 1; |
| 611 | fetchType = this.FetchType.FillGap; |
| 612 | } |
| 613 | }else{ |
| 614 | /* Prepend to next TR */ |
| 615 | if(!this.pos.next){ |
| 616 | console.error("Attempt to fetch previous diff lines but don't have any."); |
| 617 | return this; |
| 618 | } |
| 619 | up.to = this.pos.next.startLhs - 1; |
| 620 | up.from = Math.max(1, up.to - Diff.config.chunkLoadLines + 1); |
| 621 | if( this.pos.prev && this.pos.prev.endLhs >= up.from ){ |
| 622 | up.from = this.pos.prev.endLhs + 1; |
| 623 | fetchType = this.FetchType.FillGap; |
| 624 | } |
| 625 | } |
| 626 | this.$isFetching = true; |
| 627 | console.debug("fetchChunk(",fetchType,")",up); |
| 628 | Diff.fetchArtifactChunk(fOpt); |
| 629 | return this; |
| 630 | } |
| 631 | }; |
| 632 | |
| 633 | const addDiffSkipHandlers = function(){ |
| 634 | const tables = document.querySelectorAll('table.diff[data-lefthash]:not(.diffskipped)'); |
| 635 | /* Potential performance-related TODO: instead of installing all |
| 636 | of these at once, install them as the corresponding TR is |
| 637 | scrolled into view. */ |
| 638 | tables.forEach(function(table){ |
| 639 | D.addClass(table, 'diffskipped'/*avoid processing these more than once */); |
| 640 | table.querySelectorAll('tr.diffskip[data-startln]').forEach(function(tr){ |
| 641 | new ChunkLoadControls(D.addClass(tr, 'jchunk')); |
| 642 | }); |
| 643 | }); |
| 644 | return F; |
| 645 | }; |
| 646 | addDiffSkipHandlers(); |
| 647 | }); |
| 648 | |
| 649 | /* Refinements to the display of unified and side-by-side diffs. |
| 650 | ** |
| 651 | ** In all cases, the table columns tagged with "difftxt" are expanded, |
| @@ -619,10 +682,11 @@ | |
| 682 | if(!f.allDiffs){ |
| 683 | f.allDiffs = document.querySelectorAll('table.diff'); |
| 684 | } |
| 685 | w = f.lastWidth; |
| 686 | f.allDiffs.forEach((e)=>e.style.maxWidth = w + "px"); |
| 687 | //console.debug("checkTableWidth(",force,") f.lastWidth =",f.lastWidth); |
| 688 | return this; |
| 689 | }; |
| 690 | |
| 691 | const scrollLeft = function(event){ |
| 692 | //console.debug("scrollLeft",this,event); |
| 693 |