Fossil SCM

fossil-scm / src / fossil.diff.js
Source Blame History 817 lines
51c1efd… stephan 1 /**
51c1efd… stephan 2 diff-related JS APIs for fossil.
51c1efd… stephan 3 */
51c1efd… stephan 4 "use strict";
a718a76… stephan 5 /* Locate the UI element (if any) into which we can inject some diff-related
a718a76… stephan 6 UI controls. */
a718a76… stephan 7 window.fossil.onPageLoad(function(){
a718a76… stephan 8 const potentialParents = window.fossil.page.diffControlContainers = [
a718a76… stephan 9 /* CSS selectors for possible parents for injected diff-related UI
a718a76… stephan 10 controls. */
a718a76… stephan 11 /* Put the most likely pages at the end, as array.pop() is more
a718a76… stephan 12 efficient than array.shift() (see loop below). */
a718a76… stephan 13 /* /filedit */ 'body.cpage-fileedit #fileedit-tab-diff-buttons',
a718a76… stephan 14 /* /wikiedit */ 'body.cpage-wikiedit #wikiedit-tab-diff-buttons',
a718a76… stephan 15 /* /fdiff */ 'body.fdiff form div.submenu',
a718a76… stephan 16 /* /vdiff */ 'body.vdiff form div.submenu',
a718a76… stephan 17 /* /info, /vinfo, /ckout */ 'body.vinfo div.sectionmenu.info-changes-menu'
a718a76… stephan 18 ];
a718a76… stephan 19 window.fossil.page.diffControlContainer = undefined;
a718a76… stephan 20 while( potentialParents.length ){
a718a76… stephan 21 if( (window.fossil.page.diffControlContainer
a718a76… stephan 22 = document.querySelector(potentialParents.pop())) ){
a718a76… stephan 23 break;
a718a76… stephan 24 }
a718a76… stephan 25 }
a718a76… stephan 26 });
a718a76… stephan 27
51c1efd… stephan 28 window.fossil.onPageLoad(function(){
51c1efd… stephan 29 /**
51c1efd… stephan 30 Adds toggle checkboxes to each file entry in the diff views for
51c1efd… stephan 31 /info and similar pages.
51c1efd… stephan 32 */
6f26395… stephan 33 if( !window.fossil.page.diffControlContainer ){
6f26395… stephan 34 return;
6f26395… stephan 35 }
51c1efd… stephan 36 const D = window.fossil.dom;
903142f… stephan 37 const allToggles = [/*collection of all diff-toggle checkboxes*/];
903142f… stephan 38 let checkedCount =
903142f… stephan 39 0 /* When showing more than one diff, keep track of how many
d83638e… danield 40 "show/hide" checkboxes are checked so we can update the
903142f… stephan 41 "show/hide all" label dynamically. */;
6f26395… stephan 42 let btnAll /* UI control to show/hide all diffs */;
6f26395… stephan 43 /* Install a diff-toggle button for the given diff table element. */
51c1efd… stephan 44 const addToggle = function(diffElem){
51c1efd… stephan 45 const sib = diffElem.previousElementSibling,
a718a76… stephan 46 ckbox = sib ? D.addClass(D.checkbox(true), 'diff-toggle') : 0;
51c1efd… stephan 47 if(!sib) return;
a718a76… stephan 48 const lblToggle = D.label();
a718a76… stephan 49 D.append(lblToggle, ckbox, D.text(" show/hide "));
a718a76… stephan 50 allToggles.push(ckbox);
903142f… stephan 51 ++checkedCount;
6bc8c5f… stephan 52 /* Make all of the available empty space a click zone for the checkbox */
6bc8c5f… stephan 53 lblToggle.style.flexGrow = 1;
6bc8c5f… stephan 54 lblToggle.style.textAlign = 'right';
6bc8c5f… stephan 55 D.append(sib, lblToggle);
a718a76… stephan 56 ckbox.addEventListener('change', function(){
a11d245… stephan 57 diffElem.classList[this.checked ? 'remove' : 'add']('hidden');
903142f… stephan 58 if(btnAll){
903142f… stephan 59 checkedCount += (this.checked ? 1 : -1);
903142f… stephan 60 btnAll.innerText = (checkedCount < allToggles.length)
903142f… stephan 61 ? "Show diffs" : "Hide diffs";
7f2b3a2… stephan 62 }
7f2b3a2… stephan 63 }, false);
7f2b3a2… stephan 64 /* Extend the toggle click zone to all of the non-hyperlink
7f2b3a2… stephan 65 elements in the left of this area (filenames and hashes). */
7f2b3a2… stephan 66 sib.firstElementChild.addEventListener('click', (event)=>{
7f2b3a2… stephan 67 if( event.target===sib.firstElementChild ){
7f2b3a2… stephan 68 /* Don't respond to clicks bubbling via hyperlink children */
88d8255… stephan 69 ckbox.click();;
6f26395… stephan 70 }
6f26395… stephan 71 }, false);
a11d245… stephan 72 };
a11d245… stephan 73 if( !document.querySelector('body.fdiff') ){
a11d245… stephan 74 /* Don't show the diff toggle button for /fdiff because it only
a11d245… stephan 75 has a single file to show (and also a different DOM layout). */
a11d245… stephan 76 document.querySelectorAll('table.diff').forEach(addToggle);
a718a76… stephan 77 }
6f26395… stephan 78 /**
6f26395… stephan 79 Set up a "toggle all diffs" button which toggles all of the
6f26395… stephan 80 above-installed checkboxes, but only if more than one diff is
6f26395… stephan 81 rendered.
6f26395… stephan 82 */
a718a76… stephan 83 const icm = allToggles.length>1 ? window.fossil.page.diffControlContainer : 0;
a718a76… stephan 84 if(icm) {
903142f… stephan 85 btnAll = D.addClass(D.a("#", "Hide diffs"), "button");
a718a76… stephan 86 D.append( icm, btnAll );
a718a76… stephan 87 btnAll.addEventListener('click', function(ev){
a718a76… stephan 88 ev.preventDefault();
a718a76… stephan 89 ev.stopPropagation();
903142f… stephan 90 const show = checkedCount < allToggles.length;
903142f… stephan 91 for( const ckbox of allToggles ){
a718a76… stephan 92 /* Toggle all entries to match this new state. We use click()
a718a76… stephan 93 instead of ckbox.checked=... so that the on-change event handler
a718a76… stephan 94 fires. */
a718a76… stephan 95 if(ckbox.checked!==show) ckbox.click();
a718a76… stephan 96 }
a718a76… stephan 97 }, false);
a11d245… stephan 98 }
51c1efd… stephan 99 });
51c1efd… stephan 100
51c1efd… stephan 101 window.fossil.onPageLoad(function(){
51c1efd… stephan 102 const F = window.fossil, D = F.dom;
51c1efd… stephan 103 const Diff = F.diff = {
51c1efd… stephan 104 e:{/*certain cached DOM elements*/},
51c1efd… stephan 105 config: {
51c1efd… stephan 106 chunkLoadLines: (
51c1efd… stephan 107 F.config.diffContextLines * 3
51c1efd… stephan 108 /*per /chat discussion*/
51c1efd… stephan 109 ) || 20,
51c1efd… stephan 110 chunkFetch: {
e2bdc10… danield 111 /* Default callback handlers for Diff.fetchArtifactChunk(),
51c1efd… stephan 112 unless overridden by options passeed to that function. */
51c1efd… stephan 113 beforesend: function(){},
51c1efd… stephan 114 aftersend: function(){},
51c1efd… stephan 115 onerror: function(e){
ba40082… stephan 116 console.error("XHR error: ",e);
51c1efd… stephan 117 }
51c1efd… stephan 118 }
51c1efd… stephan 119 }
51c1efd… stephan 120 };
51c1efd… stephan 121 /**
51c1efd… stephan 122 Uses the /jchunk AJAX route to fetch specific lines of a given
51c1efd… stephan 123 artifact. The argument must be an Object suitable for passing as
51c1efd… stephan 124 the second argument to fossil.fetch(). Its urlParams property
51c1efd… stephan 125 must be an object with these properties:
51c1efd… stephan 126
51c1efd… stephan 127 {
51c1efd… stephan 128 name: full hash of the target file,
51c1efd… stephan 129 from: first 1-based line number of the file to fetch (inclusive),
51c1efd… stephan 130 to: last 1-based line number of the file to fetch (inclusive)
51c1efd… stephan 131 }
51c1efd… stephan 132
51c1efd… stephan 133 The fetchOpt object is NOT cloned for use by the call: it is used
51c1efd… stephan 134 as-is and may be modified by this call. Thus callers "really
51c1efd… stephan 135 should" pass a temporary object, not a long-lived one.
51c1efd… stephan 136
51c1efd… stephan 137 If fetchOpt does not define any of the (beforesend, aftersend,
51c1efd… stephan 138 onerror) callbacks, the defaults from fossil.diff.config.chunkFetch
51c1efd… stephan 139 are used, so any given client page may override those to provide
51c1efd… stephan 140 page-level default handling.
51c1efd… stephan 141
51c1efd… stephan 142 Note that onload callback is ostensibly optional but this
51c1efd… stephan 143 function is not of much use without an onload
51c1efd… stephan 144 handler. Conversely, the default onerror handler is often
51c1efd… stephan 145 customized on a per-page basis to send the error output somewhere
51c1efd… stephan 146 where the user can see it.
51c1efd… stephan 147
51c1efd… stephan 148 The response, on success, will be an array of strings, each entry
51c1efd… stephan 149 being one line from the requested artifact. If the 'to' line is
51c1efd… stephan 150 greater than the length of the file, the array will be shorter
51c1efd… stephan 151 than (to-from) lines.
51c1efd… stephan 152
51c1efd… stephan 153 The /jchunk route reports errors via JSON objects with
51c1efd… stephan 154 an "error" string property describing the problem.
51c1efd… stephan 155
51c1efd… stephan 156 This is an async operation. Returns the fossil object.
51c1efd… stephan 157 */
51c1efd… stephan 158 Diff.fetchArtifactChunk = function(fetchOpt){
51c1efd… stephan 159 if(!fetchOpt.beforesend) fetchOpt.beforesend = Diff.config.chunkFetch.beforesend;
51c1efd… stephan 160 if(!fetchOpt.aftersend) fetchOpt.aftersend = Diff.config.chunkFetch.aftersend;
51c1efd… stephan 161 if(!fetchOpt.onerror) fetchOpt.onerror = Diff.config.chunkFetch.onerror;
51c1efd… stephan 162 fetchOpt.responseType = 'json';
51c1efd… stephan 163 return F.fetch('jchunk', fetchOpt);
51c1efd… stephan 164 };
51c1efd… stephan 165
51c1efd… stephan 166
51c1efd… stephan 167 /**
51c1efd… stephan 168 Extracts either the starting or ending line number from a
51c1efd… stephan 169 line-numer column in the given tr. isSplit must be true if tr
51c1efd… stephan 170 represents a split diff, else false. Expects its tr to be valid:
51c1efd… stephan 171 GIGO applies. Returns the starting line number if getStart, else
51c1efd… stephan 172 the ending line number. Returns the line number from the LHS file
51c1efd… stephan 173 if getLHS is true, else the RHS.
51c1efd… stephan 174 */
51c1efd… stephan 175 const extractLineNo = function f(getLHS, getStart, tr, isSplit){
51c1efd… stephan 176 if(!f.rx){
51c1efd… stephan 177 f.rx = {
51c1efd… stephan 178 start: /^\s*(\d+)/,
51c1efd… stephan 179 end: /(\d+)\n?$/
51c1efd… stephan 180 }
51c1efd… stephan 181 }
51c1efd… stephan 182 const td = tr.querySelector('td:nth-child('+(
51c1efd… stephan 183 /* TD element with the line numbers */
51c1efd… stephan 184 getLHS ? 1 : (isSplit ? 4 : 2)
51c1efd… stephan 185 )+')');
51c1efd… stephan 186 const m = f.rx[getStart ? 'start' : 'end'].exec(td.innerText);
51c1efd… stephan 187 return m ? +m[1] : undefined/*"shouldn't happen"*/;
51c1efd… stephan 188 };
71e9ca7… stephan 189
51c1efd… stephan 190 /**
51c1efd… stephan 191 Installs chunk-loading controls into TR.diffskip element tr.
51c1efd… stephan 192 Each instance corresponds to a single TR.diffskip element.
51c1efd… stephan 193
51c1efd… stephan 194 The goal is to base these controls roughly on github's, a good
51c1efd… stephan 195 example of which, for use as a model, is:
51c1efd… stephan 196
51c1efd… stephan 197 https://github.com/msteveb/autosetup/commit/235925e914a52a542
51c1efd… stephan 198 */
51c1efd… stephan 199 const ChunkLoadControls = function(tr){
a49393a… stephan 200 this.$fetchQueue = [];
51c1efd… stephan 201 this.e = {/*DOM elements*/
51c1efd… stephan 202 tr: tr,
51c1efd… stephan 203 table: tr.parentElement/*TBODY*/.parentElement
51c1efd… stephan 204 };
51c1efd… stephan 205 this.isSplit = this.e.table.classList.contains('splitdiff')/*else udiff*/;
51c1efd… stephan 206 this.fileHash = this.e.table.dataset.lefthash;
51c1efd… stephan 207 tr.$chunker = this /* keep GC from reaping this */;
51c1efd… stephan 208 this.pos = {
51c1efd… stephan 209 /* These line numbers correspond to the LHS file. Because the
51c1efd… stephan 210 contents are common to both sides, we have the same number
51c1efd… stephan 211 for the RHS, but need to extract those line numbers from the
51c1efd… stephan 212 neighboring TR blocks */
51c1efd… stephan 213 startLhs: +tr.dataset.startln,
51c1efd… stephan 214 endLhs: +tr.dataset.endln
51c1efd… stephan 215 };
51c1efd… stephan 216 D.clearElement(tr);
51c1efd… stephan 217 this.e.td = D.addClass(
51c1efd… stephan 218 /* Holder for our UI controls */
51c1efd… stephan 219 D.attr(D.td(tr), 'colspan', this.isSplit ? 5 : 4),
51c1efd… stephan 220 'chunkctrl'
51c1efd… stephan 221 );
ba40082… stephan 222 this.e.msgWidget = D.addClass(D.span(), 'hidden');
51c1efd… stephan 223 this.e.btnWrapper = D.div();
51c1efd… stephan 224 D.append(this.e.td, this.e.btnWrapper);
51c1efd… stephan 225 /**
51c1efd… stephan 226 Depending on various factors, we need one or more of:
51c1efd… stephan 227
51c1efd… stephan 228 - A single button to load the initial chunk incrementally
51c1efd… stephan 229
51c1efd… stephan 230 - A single button to load all lines then remove this control
51c1efd… stephan 231
51c1efd… stephan 232 - Two buttons: one to load upwards, one to load downwards
51c1efd… stephan 233
51c1efd… stephan 234 - A single button to load the final chunk incrementally
51c1efd… stephan 235 */
51c1efd… stephan 236 if(tr.nextElementSibling){
51c1efd… stephan 237 this.pos.next = {
51c1efd… stephan 238 startLhs: extractLineNo(true, true, tr.nextElementSibling, this.isSplit),
51c1efd… stephan 239 startRhs: extractLineNo(false, true, tr.nextElementSibling, this.isSplit)
51c1efd… stephan 240 };
51c1efd… stephan 241 }
51c1efd… stephan 242 if(tr.previousElementSibling){
51c1efd… stephan 243 this.pos.prev = {
51c1efd… stephan 244 endLhs: extractLineNo(true, false, tr.previousElementSibling, this.isSplit),
51c1efd… stephan 245 endRhs: extractLineNo(false, false, tr.previousElementSibling, this.isSplit)
51c1efd… stephan 246 };
51c1efd… stephan 247 }
51c1efd… stephan 248 let btnUp = false, btnDown = false;
51c1efd… stephan 249 /**
51c1efd… stephan 250 this.pos.next refers to the line numbers in the next TR's chunk.
51c1efd… stephan 251 this.pos.prev refers to the line numbers in the previous TR's chunk.
8347c4a… florian 252 this.pos corresponds to the line numbers of the gap.
51c1efd… stephan 253 */
7b1e2aa… stephan 254 if(this.pos.prev && this.pos.next
8347c4a… florian 255 && ((this.pos.endLhs - this.pos.startLhs)
7b1e2aa… stephan 256 <= Diff.config.chunkLoadLines)){
51c1efd… stephan 257 /* Place a single button to load the whole block, rather
51c1efd… stephan 258 than separate up/down buttons. */
51c1efd… stephan 259 btnDown = false;
51c1efd… stephan 260 btnUp = this.createButton(this.FetchType.FillGap);
51c1efd… stephan 261 }else{
51c1efd… stephan 262 /* Figure out which chunk-load buttons to add... */
51c1efd… stephan 263 if(this.pos.prev){
51c1efd… stephan 264 btnDown = this.createButton(this.FetchType.PrevDown);
51c1efd… stephan 265 }
51c1efd… stephan 266 if(this.pos.next){
51c1efd… stephan 267 btnUp = this.createButton(this.FetchType.NextUp);
51c1efd… stephan 268 }
51c1efd… stephan 269 }
51c1efd… stephan 270 //this.e.btnUp = btnUp;
51c1efd… stephan 271 //this.e.btnDown = btnDown;
51c1efd… stephan 272 if(btnUp) D.append(this.e.btnWrapper, btnUp);
4e45fcc… stephan 273 if(btnDown) D.append(this.e.btnWrapper, btnDown);
ba40082… stephan 274 D.append(this.e.btnWrapper, this.e.msgWidget);
51c1efd… stephan 275 /* For debugging only... */
51c1efd… stephan 276 this.e.posState = D.span();
51c1efd… stephan 277 D.append(this.e.btnWrapper, this.e.posState);
51c1efd… stephan 278 this.updatePosDebug();
51c1efd… stephan 279 };
51c1efd… stephan 280
51c1efd… stephan 281 ChunkLoadControls.prototype = {
51c1efd… stephan 282 /** An "enum" of values describing the types of context
51c1efd… stephan 283 fetches/operations performed by this type. The values in this
51c1efd… stephan 284 object must not be changed without modifying all logic which
51c1efd… stephan 285 relies on their relative order. */
51c1efd… stephan 286 FetchType:{
51c1efd… stephan 287 /** Append context to the bottom of the previous diff chunk. */
51c1efd… stephan 288 PrevDown: 1,
51c1efd… stephan 289 /** Fill a complete gap between the previous/next diff chunks
51c1efd… stephan 290 or at the start of the next chunk or end of the previous
51c1efd… stephan 291 chunks. */
51c1efd… stephan 292 FillGap: 0,
51c1efd… stephan 293 /** Prepend context to the start of the next diff chunk. */
a49393a… stephan 294 NextUp: -1,
a49393a… stephan 295 /** Process the next queued action. */
a49393a… stephan 296 ProcessQueue: 0x7fffffff
51c1efd… stephan 297 },
51c1efd… stephan 298
51c1efd… stephan 299 /**
51c1efd… stephan 300 Creates and returns a button element for fetching a chunk in
51c1efd… stephan 301 the given fetchType (as documented for fetchChunk()).
51c1efd… stephan 302 */
51c1efd… stephan 303 createButton: function(fetchType){
51c1efd… stephan 304 let b;
51c1efd… stephan 305 switch(fetchType){
51c1efd… stephan 306 case this.FetchType.PrevDown:
51c1efd… stephan 307 b = D.append(
51c1efd… stephan 308 D.addClass(D.span(), 'down'),
51c1efd… stephan 309 D.span(/*glyph holder*/)
51c1efd… stephan 310 );
51c1efd… stephan 311 break;
51c1efd… stephan 312 case this.FetchType.FillGap:
51c1efd… stephan 313 b = D.append(
51c1efd… stephan 314 D.addClass(D.span(), 'up', 'down'),
51c1efd… stephan 315 D.span(/*glyph holder*/)
51c1efd… stephan 316 );
51c1efd… stephan 317 break;
51c1efd… stephan 318 case this.FetchType.NextUp:
51c1efd… stephan 319 b = D.append(
51c1efd… stephan 320 D.addClass(D.span(), 'up'),
51c1efd… stephan 321 D.span(/*glyph holder*/)
51c1efd… stephan 322 );
51c1efd… stephan 323 break;
51c1efd… stephan 324 default:
51c1efd… stephan 325 throw new Error("Internal API misuse: unexpected fetchType value "+fetchType);
51c1efd… stephan 326 }
51c1efd… stephan 327 D.addClass(b, 'jcbutton');
51c1efd… stephan 328 b.addEventListener('click', ()=>this.fetchChunk(fetchType),false);
51c1efd… stephan 329 return b;
51c1efd… stephan 330 },
51c1efd… stephan 331
51c1efd… stephan 332 updatePosDebug: function(){
51c1efd… stephan 333 if(this.e.posState){
51c1efd… stephan 334 D.clearElement(this.e.posState);
51c1efd… stephan 335 //D.append(D.clearElement(this.e.posState), JSON.stringify(this.pos));
51c1efd… stephan 336 }
51c1efd… stephan 337 return this;
51c1efd… stephan 338 },
51c1efd… stephan 339
51c1efd… stephan 340 /* Attempt to clean up resources and remove some circular references to
51c1efd… stephan 341 that GC can do the right thing. */
51c1efd… stephan 342 destroy: function(){
a49393a… stephan 343 delete this.$fetchQueue;
51c1efd… stephan 344 D.remove(this.e.tr);
51c1efd… stephan 345 delete this.e.tr.$chunker;
51c1efd… stephan 346 delete this.e.tr;
51c1efd… stephan 347 delete this.e;
51c1efd… stephan 348 delete this.pos;
51c1efd… stephan 349 },
51c1efd… stephan 350
51c1efd… stephan 351 /**
51c1efd… stephan 352 If the gap between this.pos.endLhs/startLhs is less than or equal to
51c1efd… stephan 353 Diff.config.chunkLoadLines then this function replaces any up/down buttons
51c1efd… stephan 354 with a gap-filler button, else it's a no-op. Returns this object.
7b1e2aa… stephan 355
7b1e2aa… stephan 356 As a special case, do not apply this at the start or bottom
7b1e2aa… stephan 357 of the diff, only between two diff chunks.
7b1e2aa… stephan 358 */
51c1efd… stephan 359 maybeReplaceButtons: function(){
7b1e2aa… stephan 360 if(this.pos.next && this.pos.prev
7b1e2aa… stephan 361 && (this.pos.endLhs - this.pos.startLhs <= Diff.config.chunkLoadLines)){
51c1efd… stephan 362 D.clearElement(this.e.btnWrapper);
51c1efd… stephan 363 D.append(this.e.btnWrapper, this.createButton(this.FetchType.FillGap));
4b1cf8d… florian 364 if( this.$fetchQueue && this.$fetchQueue.length>1 ){
4b1cf8d… florian 365 this.$fetchQueue[1] = this.FetchType.FillGap;
4b1cf8d… florian 366 this.$fetchQueue.length = 2;
a49393a… stephan 367 }
51c1efd… stephan 368 }
51c1efd… stephan 369 return this;
51c1efd… stephan 370 },
51c1efd… stephan 371
51c1efd… stephan 372 /**
4d575dc… florian 373 Callback for /jchunk responses.
51c1efd… stephan 374 */
51c1efd… stephan 375 injectResponse: function f(fetchType/*as for fetchChunk()*/,
51c1efd… stephan 376 urlParam/*from fetchChunk()*/,
51c1efd… stephan 377 lines/*response lines*/){
51c1efd… stephan 378 if(!lines.length){
51c1efd… stephan 379 /* No more data to load */
51c1efd… stephan 380 this.destroy();
51c1efd… stephan 381 return this;
51c1efd… stephan 382 }
ba40082… stephan 383 this.msg(false);
51c1efd… stephan 384 //console.debug("Loaded line range ",
51c1efd… stephan 385 //urlParam.from,"-",urlParam.to, "fetchType ",fetchType);
51c1efd… stephan 386 const lineno = [],
51c1efd… stephan 387 trPrev = this.e.tr.previousElementSibling,
51c1efd… stephan 388 trNext = this.e.tr.nextElementSibling,
51c1efd… stephan 389 doAppend = (
51c1efd… stephan 390 !!trPrev && fetchType>=this.FetchType.FillGap
51c1efd… stephan 391 ) /* true to append to previous TR, else prepend to NEXT TR */;
51c1efd… stephan 392 const tr = doAppend ? trPrev : trNext;
51c1efd… stephan 393 const joinTr = (
51c1efd… stephan 394 this.FetchType.FillGap===fetchType && trPrev && trNext
51c1efd… stephan 395 ) ? trNext : false
51c1efd… stephan 396 /* Truthy if we want to combine trPrev, the new content, and
51c1efd… stephan 397 trNext into trPrev and then remove trNext. */;
51c1efd… stephan 398 let i, td;
51c1efd… stephan 399 if(!f.convertLines){
4888719… stephan 400 /* Reminder: string.replaceAll() is a relatively new
4888719… stephan 401 JS feature, not available in some still-widely-used
4888719… stephan 402 browser versions. */
4888719… stephan 403 f.rx = [[/&/g, '&amp;'], [/</g, '&lt;']];
51c1efd… stephan 404 f.convertLines = function(li){
4888719… stephan 405 var s = li.join('\n');
4888719… stephan 406 f.rx.forEach((a)=>s=s.replace(a[0],a[1]));
4888719… stephan 407 return s + '\n';
51c1efd… stephan 408 };
51c1efd… stephan 409 }
51c1efd… stephan 410 if(1){ // LHS line numbers...
51c1efd… stephan 411 const selector = '.difflnl > pre';
51c1efd… stephan 412 td = tr.querySelector(selector);
51c1efd… stephan 413 const lnTo = Math.min(urlParam.to,
51c1efd… stephan 414 urlParam.from +
51c1efd… stephan 415 lines.length - 1/*b/c request range is inclusive*/);
51c1efd… stephan 416 for( i = urlParam.from; i <= lnTo; ++i ){
51c1efd… stephan 417 lineno.push(i);
51c1efd… stephan 418 }
51c1efd… stephan 419 const lineNoTxt = lineno.join('\n')+'\n';
51c1efd… stephan 420 const content = [td.innerHTML];
51c1efd… stephan 421 if(doAppend) content.push(lineNoTxt);
51c1efd… stephan 422 else content.unshift(lineNoTxt);
51c1efd… stephan 423 if(joinTr){
51c1efd… stephan 424 content.push(trNext.querySelector(selector).innerHTML);
51c1efd… stephan 425 }
51c1efd… stephan 426 td.innerHTML = content.join('');
51c1efd… stephan 427 }
51c1efd… stephan 428
51c1efd… stephan 429 if(1){// code block(s)...
51c1efd… stephan 430 const selector = '.difftxt > pre';
51c1efd… stephan 431 td = tr.querySelectorAll(selector);
51c1efd… stephan 432 const code = f.convertLines(lines);
4888719… stephan 433 let joinNdx = 0/*selector[X] index to join together*/;
51c1efd… stephan 434 td.forEach(function(e){
51c1efd… stephan 435 const content = [e.innerHTML];
51c1efd… stephan 436 if(doAppend) content.push(code);
51c1efd… stephan 437 else content.unshift(code);
51c1efd… stephan 438 if(joinTr){
51c1efd… stephan 439 content.push(trNext.querySelectorAll(selector)[joinNdx++].innerHTML)
51c1efd… stephan 440 }
51c1efd… stephan 441 e.innerHTML = content.join('');
51c1efd… stephan 442 });
51c1efd… stephan 443 }
51c1efd… stephan 444
51c1efd… stephan 445 if(1){// Add blank lines in (.diffsep>pre)
51c1efd… stephan 446 const selector = '.diffsep > pre';
51c1efd… stephan 447 td = tr.querySelector(selector);
51c1efd… stephan 448 for(i = 0; i < lineno.length; ++i) lineno[i] = '';
51c1efd… stephan 449 const blanks = lineno.join('\n')+'\n';
51c1efd… stephan 450 const content = [td.innerHTML];
51c1efd… stephan 451 if(doAppend) content.push(blanks);
51c1efd… stephan 452 else content.unshift(blanks);
51c1efd… stephan 453 if(joinTr){
51c1efd… stephan 454 content.push(trNext.querySelector(selector).innerHTML);
51c1efd… stephan 455 }
51c1efd… stephan 456 td.innerHTML = content.join('');
51c1efd… stephan 457 }
51c1efd… stephan 458
51c1efd… stephan 459 if(this.FetchType.FillGap===fetchType){
51c1efd… stephan 460 /* Closing the whole gap between two chunks or a whole gap
51c1efd… stephan 461 at the start or end of a diff. */
51c1efd… stephan 462 // RHS line numbers...
51c1efd… stephan 463 let startLnR = this.pos.prev
51c1efd… stephan 464 ? this.pos.prev.endRhs+1 /* Closing the whole gap between two chunks
51c1efd… stephan 465 or end-of-file gap. */
51c1efd… stephan 466 : this.pos.next.startRhs - lines.length /* start-of-file gap */;
51c1efd… stephan 467 lineno.length = lines.length;
51c1efd… stephan 468 for( i = startLnR; i < startLnR + lines.length; ++i ){
51c1efd… stephan 469 lineno[i-startLnR] = i;
51c1efd… stephan 470 }
51c1efd… stephan 471 const selector = '.difflnr > pre';
51c1efd… stephan 472 td = tr.querySelector(selector);
51c1efd… stephan 473 const lineNoTxt = lineno.join('\n')+'\n';
51c1efd… stephan 474 lineno.length = 0;
51c1efd… stephan 475 const content = [td.innerHTML];
51c1efd… stephan 476 if(doAppend) content.push(lineNoTxt);
51c1efd… stephan 477 else content.unshift(lineNoTxt);
51c1efd… stephan 478 if(joinTr){
51c1efd… stephan 479 content.push(trNext.querySelector(selector).innerHTML);
51c1efd… stephan 480 }
51c1efd… stephan 481 td.innerHTML = content.join('');
51c1efd… stephan 482 if(joinTr) D.remove(joinTr);
51c1efd… stephan 483 this.destroy();
51c1efd… stephan 484 return this;
51c1efd… stephan 485 }else if(this.FetchType.PrevDown===fetchType){
51c1efd… stephan 486 /* Append context to previous TR. */
51c1efd… stephan 487 // RHS line numbers...
51c1efd… stephan 488 let startLnR = this.pos.prev.endRhs+1;
51c1efd… stephan 489 lineno.length = lines.length;
51c1efd… stephan 490 for( i = startLnR; i < startLnR + lines.length; ++i ){
51c1efd… stephan 491 lineno[i-startLnR] = i;
51c1efd… stephan 492 }
51c1efd… stephan 493 this.pos.startLhs += lines.length;
51c1efd… stephan 494 this.pos.prev.endRhs += lines.length;
51c1efd… stephan 495 this.pos.prev.endLhs += lines.length;
51c1efd… stephan 496 const selector = '.difflnr > pre';
51c1efd… stephan 497 td = tr.querySelector(selector);
51c1efd… stephan 498 const lineNoTxt = lineno.join('\n')+'\n';
51c1efd… stephan 499 lineno.length = 0;
51c1efd… stephan 500 const content = [td.innerHTML];
51c1efd… stephan 501 if(doAppend) content.push(lineNoTxt);
51c1efd… stephan 502 else content.unshift(lineNoTxt);
51c1efd… stephan 503 td.innerHTML = content.join('');
51c1efd… stephan 504 if(lines.length < (urlParam.to - urlParam.from)){
51c1efd… stephan 505 /* No more data. */
51c1efd… stephan 506 this.destroy();
51c1efd… stephan 507 }else{
51c1efd… stephan 508 this.maybeReplaceButtons();
51c1efd… stephan 509 this.updatePosDebug();
51c1efd… stephan 510 }
51c1efd… stephan 511 return this;
51c1efd… stephan 512 }else if(this.FetchType.NextUp===fetchType){
51c1efd… stephan 513 /* Prepend content to next TR. */
51c1efd… stephan 514 // RHS line numbers...
51c1efd… stephan 515 if(doAppend){
51c1efd… stephan 516 throw new Error("Internal precondition violation: doAppend is true.");
51c1efd… stephan 517 }
51c1efd… stephan 518 let startLnR = this.pos.next.startRhs - lines.length;
51c1efd… stephan 519 lineno.length = lines.length;
51c1efd… stephan 520 for( i = startLnR; i < startLnR + lines.length; ++i ){
51c1efd… stephan 521 lineno[i-startLnR] = i;
51c1efd… stephan 522 }
51c1efd… stephan 523 this.pos.endLhs -= lines.length;
51c1efd… stephan 524 this.pos.next.startRhs -= lines.length;
51c1efd… stephan 525 this.pos.next.startLhs -= lines.length;
51c1efd… stephan 526 const selector = '.difflnr > pre';
51c1efd… stephan 527 td = tr.querySelector(selector);
51c1efd… stephan 528 const lineNoTxt = lineno.join('\n')+'\n';
51c1efd… stephan 529 lineno.length = 0;
51c1efd… stephan 530 td.innerHTML = lineNoTxt + td.innerHTML;
36bec9a… stephan 531 if(this.pos.endLhs<1
51c1efd… stephan 532 || lines.length < (urlParam.to - urlParam.from)){
51c1efd… stephan 533 /* No more data. */
51c1efd… stephan 534 this.destroy();
51c1efd… stephan 535 }else{
51c1efd… stephan 536 this.maybeReplaceButtons();
51c1efd… stephan 537 this.updatePosDebug();
51c1efd… stephan 538 }
51c1efd… stephan 539 return this;
51c1efd… stephan 540 }else{
51c1efd… stephan 541 throw new Error("Unexpected 'fetchType' value.");
51c1efd… stephan 542 }
ba40082… stephan 543 },
ba40082… stephan 544
ba40082… stephan 545 /**
ba40082… stephan 546 Sets this widget's message to the given text. If the message
ba40082… stephan 547 represents an error, the first argument must be truthy, else it
ba40082… stephan 548 must be falsy. Returns this object.
ba40082… stephan 549 */
ba40082… stephan 550 msg: function(isError,txt){
ba40082… stephan 551 if(txt){
ba40082… stephan 552 if(isError) D.addClass(this.e.msgWidget, 'error');
ba40082… stephan 553 else D.removeClass(this.e.msgWidget, 'error');
ba40082… stephan 554 D.append(
ba40082… stephan 555 D.removeClass(D.clearElement(this.e.msgWidget), 'hidden'),
ba40082… stephan 556 txt);
ba40082… stephan 557 }else{
ba40082… stephan 558 D.addClass(D.clearElement(this.e.msgWidget), 'hidden');
ba40082… stephan 559 }
ba40082… stephan 560 return this;
51c1efd… stephan 561 },
51c1efd… stephan 562
51c1efd… stephan 563 /**
51c1efd… stephan 564 Fetches and inserts a line chunk. fetchType is:
51c1efd… stephan 565
51c1efd… stephan 566 this.FetchType.NextUp = upwards from next chunk (this.pos.next)
51c1efd… stephan 567
51c1efd… stephan 568 this.FetchType.FillGap = the whole gap between this.pos.prev
51c1efd… stephan 569 and this.pos.next, or the whole gap before/after the
51c1efd… stephan 570 initial/final chunk in the diff.
51c1efd… stephan 571
51c1efd… stephan 572 this.FetchType.PrevDown = downwards from the previous chunk
51c1efd… stephan 573 (this.pos.prev)
51c1efd… stephan 574
51c1efd… stephan 575 Those values are set at the time this object is initialized but
51c1efd… stephan 576 one instance of this class may have 2 buttons, one each for
51c1efd… stephan 577 fetchTypes NextUp and PrevDown.
51c1efd… stephan 578
51c1efd… stephan 579 This is an async operation. While it is in transit, any calls
51c1efd… stephan 580 to this function will have no effect except (possibly) to emit
51c1efd… stephan 581 a warning. Returns this object.
51c1efd… stephan 582 */
51c1efd… stephan 583 fetchChunk: function(fetchType){
a49393a… stephan 584 if( !this.$fetchQueue ) return this; // HACKHACK: are we destroyed?
a49393a… stephan 585 if( fetchType==this.FetchType.ProcessQueue ){
36bec9a… stephan 586 this.$fetchQueue.shift();
a49393a… stephan 587 if( this.$fetchQueue.length==0 ) return this;
a49393a… stephan 588 //console.log('fetchChunk: processing queue ...');
a49393a… stephan 589 }
a49393a… stephan 590 else{
a49393a… stephan 591 this.$fetchQueue.push(fetchType);
a49393a… stephan 592 if( this.$fetchQueue.length!=1 ) return this;
a49393a… stephan 593 //console.log('fetchChunk: processing user input ...');
a49393a… stephan 594 }
a49393a… stephan 595 fetchType = this.$fetchQueue[0];
36bec9a… stephan 596 if( fetchType==this.FetchType.ProcessQueue ){
36bec9a… stephan 597 /* Unexpected! Clear queue so recovery (manual restart) is possible. */
36bec9a… stephan 598 this.$fetchQueue.length = 0;
36bec9a… stephan 599 return this;
36bec9a… stephan 600 }
51c1efd… stephan 601 /* Forewarning, this is a bit confusing: when fetching the
51c1efd… stephan 602 previous lines, we're doing so on behalf of the *next* diff
51c1efd… stephan 603 chunk (this.pos.next), and vice versa. */
51c1efd… stephan 604 if(fetchType===this.FetchType.NextUp && !this.pos.next
51c1efd… stephan 605 || fetchType===this.FetchType.PrevDown && !this.pos.prev){
51c1efd… stephan 606 console.error("Attempt to fetch diff lines but don't have any.");
51c1efd… stephan 607 return this;
51c1efd… stephan 608 }
ba40082… stephan 609 this.msg(false,"Fetching diff chunk...");
a49393a… stephan 610 const self = this;
51c1efd… stephan 611 const fOpt = {
51c1efd… stephan 612 urlParams:{
51c1efd… stephan 613 name: this.fileHash, from: 0, to: 0
51c1efd… stephan 614 },
a49393a… stephan 615 aftersend: ()=>this.msg(false),
a49393a… stephan 616 onload: function(list){
a49393a… stephan 617 self.injectResponse(fetchType,up,list);
a49393a… stephan 618 if( !self.$fetchQueue || self.$fetchQueue.length==0 ) return;
36bec9a… stephan 619 /* Keep queue length > 0, or clicks stalled during (unusually lengthy)
36bec9a… stephan 620 injectResponse() may sneak in as soon as setTimeout() allows, find
36bec9a… stephan 621 an empty queue, and therefore start over with queue processing. */
36bec9a… stephan 622 self.$fetchQueue[0] = self.FetchType.ProcessQueue;
a49393a… stephan 623 setTimeout(self.fetchChunk.bind(self,self.FetchType.ProcessQueue));
a49393a… stephan 624 }
51c1efd… stephan 625 };
51c1efd… stephan 626 const up = fOpt.urlParams;
51c1efd… stephan 627 if(fetchType===this.FetchType.FillGap){
51c1efd… stephan 628 /* Easiest case: filling a whole gap. */
51c1efd… stephan 629 up.from = this.pos.startLhs;
51c1efd… stephan 630 up.to = this.pos.endLhs;
51c1efd… stephan 631 }else if(this.FetchType.PrevDown===fetchType){
51c1efd… stephan 632 /* Append to previous TR. */
51c1efd… stephan 633 if(!this.pos.prev){
51c1efd… stephan 634 console.error("Attempt to fetch next diff lines but don't have any.");
51c1efd… stephan 635 return this;
51c1efd… stephan 636 }
51c1efd… stephan 637 up.from = this.pos.prev.endLhs + 1;
51c1efd… stephan 638 up.to = up.from +
51c1efd… stephan 639 Diff.config.chunkLoadLines - 1/*b/c request range is inclusive*/;
51c1efd… stephan 640 if( this.pos.next && this.pos.next.startLhs <= up.to ){
51c1efd… stephan 641 up.to = this.pos.next.startLhs - 1;
51c1efd… stephan 642 fetchType = this.FetchType.FillGap;
51c1efd… stephan 643 }
51c1efd… stephan 644 }else{
51c1efd… stephan 645 /* Prepend to next TR */
51c1efd… stephan 646 if(!this.pos.next){
51c1efd… stephan 647 console.error("Attempt to fetch previous diff lines but don't have any.");
51c1efd… stephan 648 return this;
51c1efd… stephan 649 }
51c1efd… stephan 650 up.to = this.pos.next.startLhs - 1;
51c1efd… stephan 651 up.from = Math.max(1, up.to - Diff.config.chunkLoadLines + 1);
51c1efd… stephan 652 if( this.pos.prev && this.pos.prev.endLhs >= up.from ){
51c1efd… stephan 653 up.from = this.pos.prev.endLhs + 1;
51c1efd… stephan 654 fetchType = this.FetchType.FillGap;
51c1efd… stephan 655 }
51c1efd… stephan 656 }
51c1efd… stephan 657 //console.debug("fetchChunk(",fetchType,")",up);
a49393a… stephan 658 fOpt.onerror = function(err){
49e3bf7… stephan 659 if(self.e/*guard against a late-stage onerror() call*/){
49e3bf7… stephan 660 self.msg(true,err.message);
49e3bf7… stephan 661 self.$fetchQueue.length = 0;
49e3bf7… stephan 662 }else{
a754761… stephan 663 Diff.config.chunkFetch.onerror.call(this,err);
49e3bf7… stephan 664 }
a49393a… stephan 665 };
51c1efd… stephan 666 Diff.fetchArtifactChunk(fOpt);
51c1efd… stephan 667 return this;
51c1efd… stephan 668 }
51c1efd… stephan 669 };
51c1efd… stephan 670
0cbfc02… stephan 671 /**
0cbfc02… stephan 672 Adds context-loading buttons to one or more tables. The argument
0cbfc02… stephan 673 may be a forEach-capable list of diff table elements, a query
0cbfc02… stephan 674 selector string matching 0 or more diff tables, or falsy, in
0cbfc02… stephan 675 which case all relevant diff tables are set up. It tags each
0cbfc02… stephan 676 table it processes to that it will not be processed multiple
0cbfc02… stephan 677 times by subsequent calls to this function.
0cbfc02… stephan 678
0cbfc02… stephan 679 Note that this only works for diffs which have been marked up
0cbfc02… stephan 680 with certain state, namely table.dataset.lefthash and TR
0cbfc02… stephan 681 entries which hold state related to browsing context.
0cbfc02… stephan 682 */
0cbfc02… stephan 683 Diff.setupDiffContextLoad = function(tables){
0cbfc02… stephan 684 if('string'===typeof tables){
0cbfc02… stephan 685 tables = document.querySelectorAll(tables);
0cbfc02… stephan 686 }else if(!tables){
0cbfc02… stephan 687 tables = document.querySelectorAll('table.diff[data-lefthash]:not(.diffskipped)');
0cbfc02… stephan 688 }
51c1efd… stephan 689 /* Potential performance-related TODO: instead of installing all
51c1efd… stephan 690 of these at once, install them as the corresponding TR is
51c1efd… stephan 691 scrolled into view. */
51c1efd… stephan 692 tables.forEach(function(table){
63eb9d3… stephan 693 if(table.classList.contains('diffskipped') || !table.dataset.lefthash) return;
51c1efd… stephan 694 D.addClass(table, 'diffskipped'/*avoid processing these more than once */);
51c1efd… stephan 695 table.querySelectorAll('tr.diffskip[data-startln]').forEach(function(tr){
51c1efd… stephan 696 new ChunkLoadControls(D.addClass(tr, 'jchunk'));
51c1efd… stephan 697 });
51c1efd… stephan 698 });
51c1efd… stephan 699 return F;
51c1efd… stephan 700 };
0cbfc02… stephan 701 Diff.setupDiffContextLoad();
51c1efd… stephan 702 });
c03ce0f… stephan 703 /*
c03ce0f… stephan 704 ** For a side-by-side diff, ensure that horizontal scrolling of either
c03ce0f… stephan 705 ** side of the diff is synchronized with the other side.
c03ce0f… stephan 706 */
c03ce0f… stephan 707 window.fossil.onPageLoad(function(){
c03ce0f… stephan 708 const F = window.fossil, D = F.dom, Diff = F.diff;
c03ce0f… stephan 709
c03ce0f… stephan 710 /* Look for a parent element to hold the sbs-sync-scroll toggle
c03ce0f… stephan 711 checkbox. This differs per page. If we don't find one, simply
c03ce0f… stephan 712 elide that toggle and use whatever preference the user last
c03ce0f… stephan 713 specified (defaulting to on). */
c03ce0f… stephan 714 let cbSync /* scroll-sync checkbox */;
a718a76… stephan 715 let eToggleParent = /* element to put the sync-scroll checkbox in */
a718a76… stephan 716 document.querySelector('table.diff.splitdiff')
a718a76… stephan 717 ? window.fossil.page.diffControlContainer
a718a76… stephan 718 : undefined;
a718a76… stephan 719 const keySbsScroll = 'sync-diff-scroll' /* F.storage key for persistent user preference */;
c03ce0f… stephan 720 if( eToggleParent ){
c03ce0f… stephan 721 /* Add a checkbox to toggle sbs scroll sync. Remember that in
c03ce0f… stephan 722 order to be UI-consistent in the /vdiff page we have to ensure
d83638e… danield 723 that the checkbox is to the LEFT of its label. We store the
c03ce0f… stephan 724 sync-scroll preference in F.storage (not a cookie) so that it
c03ce0f… stephan 725 persists across page loads and different apps. */
c03ce0f… stephan 726 cbSync = D.checkbox(keySbsScroll, F.storage.getBool(keySbsScroll,true));
c03ce0f… stephan 727 D.append(eToggleParent, D.append(
c03ce0f… stephan 728 D.addClass(D.create('span'), 'input-with-label'),
c03ce0f… stephan 729 D.append(D.create('label'),
4d2d62d… drh 730 cbSync, "Scroll Sync")
c03ce0f… stephan 731 ));
c03ce0f… stephan 732 cbSync.addEventListener('change', function(e){
c03ce0f… stephan 733 F.storage.set(keySbsScroll, e.target.checked);
c03ce0f… stephan 734 });
c03ce0f… stephan 735 }
c03ce0f… stephan 736 const useSync = cbSync ? ()=>cbSync.checked : ()=>F.storage.getBool(keySbsScroll,true);
c03ce0f… stephan 737
c03ce0f… stephan 738 /* Now set up the events to enable syncronized scrolling... */
c03ce0f… stephan 739 const scrollLeft = function(event){
c03ce0f… stephan 740 const table = this.parentElement/*TD*/.parentElement/*TR*/.
c03ce0f… stephan 741 parentElement/*TBODY*/.parentElement/*TABLE*/;
c03ce0f… stephan 742 if( useSync() ){
c03ce0f… stephan 743 table.$txtPres.forEach((e)=>(e===this) ? 1 : (e.scrollLeft = this.scrollLeft));
c03ce0f… stephan 744 }
c03ce0f… stephan 745 return false;
c03ce0f… stephan 746 };
c03ce0f… stephan 747 const SCROLL_LEN = 64/* pixels to scroll for keyboard events */;
c03ce0f… stephan 748 const keycodes = Object.assign(Object.create(null),{
c03ce0f… stephan 749 37/*cursor left*/: -SCROLL_LEN, 39/*cursor right*/: SCROLL_LEN
c03ce0f… stephan 750 });
ddeba72… stephan 751 /** keydown handler for a diff element */
ddeba72… stephan 752 const handleDiffKeydown = function(e){
ddeba72… stephan 753 const len = keycodes[e.keyCode];
ddeba72… stephan 754 if( !len ) return false;
ddeba72… stephan 755 if( useSync() ){
ddeba72… stephan 756 this.$txtPres[0].scrollLeft += len;
ddeba72… stephan 757 }else if( this.$preCurrent ){
ddeba72… stephan 758 this.$preCurrent.scrollLeft += len;
ddeba72… stephan 759 }
ddeba72… stephan 760 return false;
ddeba72… stephan 761 };
c03ce0f… stephan 762 /**
c03ce0f… stephan 763 Sets up synchronized scrolling of table.splitdiff element
c03ce0f… stephan 764 `diff`. If passed no argument, it scans the dom for elements to
c03ce0f… stephan 765 initialize. The second argument is for this function's own
c03ce0f… stephan 766 internal use.
c03ce0f… stephan 767
c03ce0f… stephan 768 It's okay (but wasteful) to pass the same element to this
c03ce0f… stephan 769 function multiple times: it will only be set up for sync
c03ce0f… stephan 770 scrolling the first time it's passed to this function.
c03ce0f… stephan 771
c03ce0f… stephan 772 Note that this setup is ignorant of the cbSync toggle: the toggle
c03ce0f… stephan 773 is checked when scrolling, not when initializing the sync-scroll
c03ce0f… stephan 774 capability.
c03ce0f… stephan 775 */
c03ce0f… stephan 776 const initTableDiff = function f(diff, unifiedDiffs){
c03ce0f… stephan 777 if(!diff){
c03ce0f… stephan 778 let i, diffs;
c03ce0f… stephan 779 diffs = document.querySelectorAll('table.splitdiff');
c03ce0f… stephan 780 for(i=0; i<diffs.length; ++i){
c03ce0f… stephan 781 f.call(this, diffs[i], false);
c03ce0f… stephan 782 }
c03ce0f… stephan 783 diffs = document.querySelectorAll('table.udiff');
c03ce0f… stephan 784 for(i=0; i<diffs.length; ++i){
c03ce0f… stephan 785 f.call(this, diffs[i], true);
c03ce0f… stephan 786 }
c03ce0f… stephan 787 return this;
c03ce0f… stephan 788 }
c03ce0f… stephan 789 diff.$txtCols = diff.querySelectorAll('td.difftxt');
c03ce0f… stephan 790 diff.$txtPres = diff.querySelectorAll('td.difftxt pre');
c03ce0f… stephan 791 if(!unifiedDiffs){
c03ce0f… stephan 792 diff.$txtPres.forEach(function(e){
c03ce0f… stephan 793 if(!e.classList.contains('scroller')){
c03ce0f… stephan 794 D.addClass(e, 'scroller');
c03ce0f… stephan 795 e.addEventListener('scroll', scrollLeft, false);
c03ce0f… stephan 796 }
c03ce0f… stephan 797 });
c03ce0f… stephan 798 diff.tabIndex = 0;
c03ce0f… stephan 799 if(!diff.classList.contains('scroller')){
c03ce0f… stephan 800 /* Keyboard-based scrolling requires special-case handling to
c03ce0f… stephan 801 ensure that we scroll the proper side of the diff when sync
c03ce0f… stephan 802 is off. */
c03ce0f… stephan 803 D.addClass(diff, 'scroller');
ddeba72… stephan 804 diff.addEventListener('keydown', handleDiffKeydown, false);
ddeba72… stephan 805 const handleLRClick = function(ev){
ddeba72… stephan 806 diff.$preCurrent = this /* NOT ev.target, which is probably a child element */;
ddeba72… stephan 807 };
ddeba72… stephan 808 diff.$txtPres.forEach((e)=>e.addEventListener('click', handleLRClick, false));
c03ce0f… stephan 809 }
c03ce0f… stephan 810 }
c03ce0f… stephan 811 return this;
c03ce0f… stephan 812 }
c03ce0f… stephan 813 window.fossil.page.tweakSbsDiffs = function(){
c03ce0f… stephan 814 document.querySelectorAll('table.splitdiff').forEach((e)=>initTableDiff(e));
c03ce0f… stephan 815 };
c03ce0f… stephan 816 initTableDiff();
c03ce0f… stephan 817 }, false);

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button