"use strict"; window.fossil.onPageLoad(function(){ const D = window.fossil.dom; const addToggle = function(diffElem){ const sib = diffElem.previousElementSibling, btn = sib ? D.addClass(D.checkbox(true), 'diff-toggle') : 0; if(!sib) return; D.append(sib,btn); btn.addEventListener('click', function(){ diffElem.classList.toggle('hidden'); }, false); }; document.querySelectorAll('table.diff').forEach(addToggle); }); window.fossil.onPageLoad(function(){ const F = window.fossil, D = F.dom; const Diff = F.diff = { e:{}, config: { chunkLoadLines: ( F.config.diffContextLines * 3 ) || 20, chunkFetch: { beforesend: function(){}, aftersend: function(){}, onerror: function(e){ console.error("XHR error: ",e); } } } }; Diff.fetchArtifactChunk = function(fetchOpt){ if(!fetchOpt.beforesend) fetchOpt.beforesend = Diff.config.chunkFetch.beforesend; if(!fetchOpt.aftersend) fetchOpt.aftersend = Diff.config.chunkFetch.aftersend; if(!fetchOpt.onerror) fetchOpt.onerror = Diff.config.chunkFetch.onerror; fetchOpt.responseType = 'json'; return F.fetch('jchunk', fetchOpt); }; const extractLineNo = function f(getLHS, getStart, tr, isSplit){ if(!f.rx){ f.rx = { start: /^\s*(\d+)/, end: /(\d+)\n?$/ } } const td = tr.querySelector('td:nth-child('+( getLHS ? 1 : (isSplit ? 4 : 2) )+')'); const m = f.rx[getStart ? 'start' : 'end'].exec(td.innerText); return m ? +m[1] : undefined; }; const ChunkLoadControls = function(tr){ this.$fetchQueue = []; this.e = { tr: tr, table: tr.parentElement.parentElement }; this.isSplit = this.e.table.classList.contains('splitdiff'); this.fileHash = this.e.table.dataset.lefthash; tr.$chunker = this; this.pos = { startLhs: +tr.dataset.startln, endLhs: +tr.dataset.endln }; D.clearElement(tr); this.e.td = D.addClass( D.attr(D.td(tr), 'colspan', this.isSplit ? 5 : 4), 'chunkctrl' ); this.e.msgWidget = D.addClass(D.span(), 'hidden'); this.e.btnWrapper = D.div(); D.append(this.e.td, this.e.btnWrapper); if(tr.nextElementSibling){ this.pos.next = { startLhs: extractLineNo(true, true, tr.nextElementSibling, this.isSplit), startRhs: extractLineNo(false, true, tr.nextElementSibling, this.isSplit) }; } if(tr.previousElementSibling){ this.pos.prev = { endLhs: extractLineNo(true, false, tr.previousElementSibling, this.isSplit), endRhs: extractLineNo(false, false, tr.previousElementSibling, this.isSplit) }; } let btnUp = false, btnDown = false; if(this.pos.prev && this.pos.next && ((this.pos.endLhs - this.pos.startLhs) <= Diff.config.chunkLoadLines)){ btnDown = false; btnUp = this.createButton(this.FetchType.FillGap); }else{ if(this.pos.prev){ btnDown = this.createButton(this.FetchType.PrevDown); } if(this.pos.next){ btnUp = this.createButton(this.FetchType.NextUp); } } if(btnUp) D.append(this.e.btnWrapper, btnUp); if(btnDown) D.append(this.e.btnWrapper, btnDown); D.append(this.e.btnWrapper, this.e.msgWidget); this.e.posState = D.span(); D.append(this.e.btnWrapper, this.e.posState); this.updatePosDebug(); }; ChunkLoadControls.prototype = { FetchType:{ PrevDown: 1, FillGap: 0, NextUp: -1, ProcessQueue: 0x7fffffff }, createButton: function(fetchType){ let b; switch(fetchType){ case this.FetchType.PrevDown: b = D.append( D.addClass(D.span(), 'down'), D.span() ); break; case this.FetchType.FillGap: b = D.append( D.addClass(D.span(), 'up', 'down'), D.span() ); break; case this.FetchType.NextUp: b = D.append( D.addClass(D.span(), 'up'), D.span() ); break; default: throw new Error("Internal API misuse: unexpected fetchType value "+fetchType); } D.addClass(b, 'jcbutton'); b.addEventListener('click', ()=>this.fetchChunk(fetchType),false); return b; }, updatePosDebug: function(){ if(this.e.posState){ D.clearElement(this.e.posState); } return this; }, destroy: function(){ delete this.$fetchQueue; D.remove(this.e.tr); delete this.e.tr.$chunker; delete this.e.tr; delete this.e; delete this.pos; }, maybeReplaceButtons: function(){ if(this.pos.next && this.pos.prev && (this.pos.endLhs - this.pos.startLhs <= Diff.config.chunkLoadLines)){ D.clearElement(this.e.btnWrapper); D.append(this.e.btnWrapper, this.createButton(this.FetchType.FillGap)); if( this.$fetchQueue && this.$fetchQueue.length>1 ){ this.$fetchQueue[1] = this.FetchType.FillGap; this.$fetchQueue.length = 2; } } return this; }, injectResponse: function f(fetchType, urlParam, lines){ if(!lines.length){ this.destroy(); return this; } this.msg(false); const lineno = [], trPrev = this.e.tr.previousElementSibling, trNext = this.e.tr.nextElementSibling, doAppend = ( !!trPrev && fetchType>=this.FetchType.FillGap ); const tr = doAppend ? trPrev : trNext; const joinTr = ( this.FetchType.FillGap===fetchType && trPrev && trNext ) ? trNext : false ; let i, td; if(!f.convertLines){ f.rx = [[/&/g, '&'], [/s=s.replace(a[0],a[1])); return s + '\n'; }; } if(1){ const selector = '.difflnl > pre'; td = tr.querySelector(selector); const lnTo = Math.min(urlParam.to, urlParam.from + lines.length - 1); for( i = urlParam.from; i <= lnTo; ++i ){ lineno.push(i); } const lineNoTxt = lineno.join('\n')+'\n'; const content = [td.innerHTML]; if(doAppend) content.push(lineNoTxt); else content.unshift(lineNoTxt); if(joinTr){ content.push(trNext.querySelector(selector).innerHTML); } td.innerHTML = content.join(''); } if(1){ const selector = '.difftxt > pre'; td = tr.querySelectorAll(selector); const code = f.convertLines(lines); let joinNdx = 0; td.forEach(function(e){ const content = [e.innerHTML]; if(doAppend) content.push(code); else content.unshift(code); if(joinTr){ content.push(trNext.querySelectorAll(selector)[joinNdx++].innerHTML) } e.innerHTML = content.join(''); }); } if(1){ const selector = '.diffsep > pre'; td = tr.querySelector(selector); for(i = 0; i < lineno.length; ++i) lineno[i] = ''; const blanks = lineno.join('\n')+'\n'; const content = [td.innerHTML]; if(doAppend) content.push(blanks); else content.unshift(blanks); if(joinTr){ content.push(trNext.querySelector(selector).innerHTML); } td.innerHTML = content.join(''); } if(this.FetchType.FillGap===fetchType){ let startLnR = this.pos.prev ? this.pos.prev.endRhs+1 : this.pos.next.startRhs - lines.length; lineno.length = lines.length; for( i = startLnR; i < startLnR + lines.length; ++i ){ lineno[i-startLnR] = i; } const selector = '.difflnr > pre'; td = tr.querySelector(selector); const lineNoTxt = lineno.join('\n')+'\n'; lineno.length = 0; const content = [td.innerHTML]; if(doAppend) content.push(lineNoTxt); else content.unshift(lineNoTxt); if(joinTr){ content.push(trNext.querySelector(selector).innerHTML); } td.innerHTML = content.join(''); if(joinTr) D.remove(joinTr); Diff.checkTableWidth(true); this.destroy(); return this; }else if(this.FetchType.PrevDown===fetchType){ let startLnR = this.pos.prev.endRhs+1; lineno.length = lines.length; for( i = startLnR; i < startLnR + lines.length; ++i ){ lineno[i-startLnR] = i; } this.pos.startLhs += lines.length; this.pos.prev.endRhs += lines.length; this.pos.prev.endLhs += lines.length; const selector = '.difflnr > pre'; td = tr.querySelector(selector); const lineNoTxt = lineno.join('\n')+'\n'; lineno.length = 0; const content = [td.innerHTML]; if(doAppend) content.push(lineNoTxt); else content.unshift(lineNoTxt); td.innerHTML = content.join(''); if(lines.length < (urlParam.to - urlParam.from)){ this.destroy(); }else{ this.maybeReplaceButtons(); this.updatePosDebug(); } Diff.checkTableWidth(true); return this; }else if(this.FetchType.NextUp===fetchType){ if(doAppend){ throw new Error("Internal precondition violation: doAppend is true."); } let startLnR = this.pos.next.startRhs - lines.length; lineno.length = lines.length; for( i = startLnR; i < startLnR + lines.length; ++i ){ lineno[i-startLnR] = i; } this.pos.endLhs -= lines.length; this.pos.next.startRhs -= lines.length; this.pos.next.startLhs -= lines.length; const selector = '.difflnr > pre'; td = tr.querySelector(selector); const lineNoTxt = lineno.join('\n')+'\n'; lineno.length = 0; td.innerHTML = lineNoTxt + td.innerHTML; if(this.pos.endLhs<1 || lines.length < (urlParam.to - urlParam.from)){ this.destroy(); }else{ this.maybeReplaceButtons(); this.updatePosDebug(); } Diff.checkTableWidth(true); return this; }else{ throw new Error("Unexpected 'fetchType' value."); } }, msg: function(isError,txt){ if(txt){ if(isError) D.addClass(this.e.msgWidget, 'error'); else D.removeClass(this.e.msgWidget, 'error'); D.append( D.removeClass(D.clearElement(this.e.msgWidget), 'hidden'), txt); }else{ D.addClass(D.clearElement(this.e.msgWidget), 'hidden'); } return this; }, fetchChunk: function(fetchType){ if( !this.$fetchQueue ) return this; if( fetchType==this.FetchType.ProcessQueue ){ this.$fetchQueue.shift(); if( this.$fetchQueue.length==0 ) return this; } else{ this.$fetchQueue.push(fetchType); if( this.$fetchQueue.length!=1 ) return this; } fetchType = this.$fetchQueue[0]; if( fetchType==this.FetchType.ProcessQueue ){ this.$fetchQueue.length = 0; return this; } if(fetchType===this.FetchType.NextUp && !this.pos.next || fetchType===this.FetchType.PrevDown && !this.pos.prev){ console.error("Attempt to fetch diff lines but don't have any."); return this; } this.msg(false,"Fetching diff chunk..."); const self = this; const fOpt = { urlParams:{ name: this.fileHash, from: 0, to: 0 }, aftersend: ()=>this.msg(false), onload: function(list){ self.injectResponse(fetchType,up,list); if( !self.$fetchQueue || self.$fetchQueue.length==0 ) return; self.$fetchQueue[0] = self.FetchType.ProcessQueue; setTimeout(self.fetchChunk.bind(self,self.FetchType.ProcessQueue)); } }; const up = fOpt.urlParams; if(fetchType===this.FetchType.FillGap){ up.from = this.pos.startLhs; up.to = this.pos.endLhs; }else if(this.FetchType.PrevDown===fetchType){ if(!this.pos.prev){ console.error("Attempt to fetch next diff lines but don't have any."); return this; } up.from = this.pos.prev.endLhs + 1; up.to = up.from + Diff.config.chunkLoadLines - 1; if( this.pos.next && this.pos.next.startLhs <= up.to ){ up.to = this.pos.next.startLhs - 1; fetchType = this.FetchType.FillGap; } }else{ if(!this.pos.next){ console.error("Attempt to fetch previous diff lines but don't have any."); return this; } up.to = this.pos.next.startLhs - 1; up.from = Math.max(1, up.to - Diff.config.chunkLoadLines + 1); if( this.pos.prev && this.pos.prev.endLhs >= up.from ){ up.from = this.pos.prev.endLhs + 1; fetchType = this.FetchType.FillGap; } } fOpt.onerror = function(err){ if(self.e){ self.msg(true,err.message); self.$fetchQueue.length = 0; }else{ Diff.config.chunkFetch.onerror.call(this,err); } }; Diff.fetchArtifactChunk(fOpt); return this; } }; Diff.setupDiffContextLoad = function(tables){ if('string'===typeof tables){ tables = document.querySelectorAll(tables); }else if(!tables){ tables = document.querySelectorAll('table.diff[data-lefthash]:not(.diffskipped)'); } tables.forEach(function(table){ if(table.classList.contains('diffskipped') || !table.dataset.lefthash) return; D.addClass(table, 'diffskipped'); table.querySelectorAll('tr.diffskip[data-startln]').forEach(function(tr){ new ChunkLoadControls(D.addClass(tr, 'jchunk')); }); }); return F; }; Diff.setupDiffContextLoad(); }); window.fossil.onPageLoad(function(){ const SCROLL_LEN = 25; const F = window.fossil, D = F.dom, Diff = F.diff; var lastWidth; Diff.checkTableWidth = function f(force){ if(undefined === f.contentNode){ f.contentNode = document.querySelector('div.content'); } force = true; const parentCS = window.getComputedStyle(f.contentNode); const parentWidth = ( f.contentNode.clientWidth - parseFloat(parentCS.marginLeft) - parseFloat(parentCS.marginRight) ); if( !force && parentWidth===lastWidth ) return this; lastWidth = parentWidth; let w = lastWidth*0.5 - 100; if(force || !f.colsL){ f.colsL = document.querySelectorAll('td.difftxtl pre'); } f.colsL.forEach(function(e){ e.style.width = w + "px"; e.style.maxWidth = w + "px"; }); if(force || !f.colsR){ f.colsR = document.querySelectorAll('td.difftxtr pre'); } f.colsR.forEach(function(e){ e.style.width = w + "px"; e.style.maxWidth = w + "px"; }); if(force || !f.colsU){ f.colsU = document.querySelectorAll('td.difftxtu pre'); } f.colsU.forEach(function(e){ w = lastWidth - 3; var k = e.parentElement; while(k = k.previousElementSibling) w -= k.scrollWidth; e.style.width = w + "px"; e.style.maxWidth = w + "px"; }); if(0){ if(!f.allDiffs){ f.allDiffs = document.querySelectorAll('table.diff'); } w = lastWidth; f.allDiffs.forEach(function f(e){ if(0 && !f.$){ f.$ = e.getClientRects()[0]; console.debug("diff table w =",w," f.$x",f.$); w - 2*f.$.x; } e.style.maxWidth = w + "px"; }); } return this; }; const scrollLeft = function(event){ const table = this.parentElement.parentElement. parentElement.parentElement; table.$txtPres.forEach((e)=>(e===this) ? 1 : (e.scrollLeft = this.scrollLeft)); return false; }; Diff.initTableDiff = function f(diff, unifiedDiffs){ if(!diff){ let i, diffs; diffs = document.querySelectorAll('table.splitdiff'); for(i=0; ie.style.width = width + 'px'); diff.$txtPres.forEach(function(e){ e.style.maxWidth = width + 'px'; e.style.width = width + 'px'; if(!unifiedDiffs && !e.classList.contains('scroller')){ D.addClass(e, 'scroller'); e.addEventListener('scroll', scrollLeft, false); } }); if(!unifiedDiffs){ diff.tabIndex = 0; if(!diff.classList.contains('scroller')){ D.addClass(diff, 'scroller'); diff.addEventListener('keydown', function(e){ e = e || event; const len = {37: -SCROLL_LEN, 39: SCROLL_LEN}[e.keyCode]; if( !len ) return; this.$txtPres[0].scrollLeft += len; return false; }, false); } } return this; } window.fossil.page.tweakSbsDiffs = function(){ document.querySelectorAll('table.splitdiff').forEach((e)=>Diff.initTableDiff(e)); Diff.checkTableWidth(); }; Diff.initTableDiff().checkTableWidth(); window.addEventListener('resize', F.debounce(()=>Diff.checkTableWidth())); }, false);