"use strict"; window.fossil.onPageLoad(function(){ const potentialParents = window.fossil.page.diffControlContainers = [ 'body.cpage-fileedit #fileedit-tab-diff-buttons', 'body.cpage-wikiedit #wikiedit-tab-diff-buttons', 'body.fdiff form div.submenu', 'body.vdiff form div.submenu', 'body.vinfo div.sectionmenu.info-changes-menu' ]; window.fossil.page.diffControlContainer = undefined; while( potentialParents.length ){ if( (window.fossil.page.diffControlContainer = document.querySelector(potentialParents.pop())) ){ break; } } }); window.fossil.onPageLoad(function(){ if( !window.fossil.page.diffControlContainer ){ return; } const D = window.fossil.dom; const allToggles = []; let checkedCount = 0; let btnAll; const addToggle = function(diffElem){ const sib = diffElem.previousElementSibling, ckbox = sib ? D.addClass(D.checkbox(true), 'diff-toggle') : 0; if(!sib) return; const lblToggle = D.label(); D.append(lblToggle, ckbox, D.text(" show/hide ")); const wrapper = D.append(D.span(), lblToggle); allToggles.push(ckbox); ++checkedCount; D.append(sib, D.append(wrapper, lblToggle)); ckbox.addEventListener('change', function(){ diffElem.classList[this.checked ? 'remove' : 'add']('hidden'); if(btnAll){ checkedCount += (this.checked ? 1 : -1); btnAll.innerText = (checkedCount < allToggles.length) ? "Show diffs" : "Hide diffs"; } }, false); }; if( !document.querySelector('body.fdiff') ){ document.querySelectorAll('table.diff').forEach(addToggle); } const icm = allToggles.length>1 ? window.fossil.page.diffControlContainer : 0; if(icm) { btnAll = D.addClass(D.a("#", "Hide diffs"), "button"); D.append( icm, btnAll ); btnAll.addEventListener('click', function(ev){ ev.preventDefault(); ev.stopPropagation(); const show = checkedCount < allToggles.length; for( const ckbox of allToggles ){ if(ckbox.checked!==show) ckbox.click(); } }, false); } }); 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); 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(); } 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(); } 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 F = window.fossil, D = F.dom, Diff = F.diff; let cbSync; let eToggleParent = document.querySelector('table.diff.splitdiff') ? window.fossil.page.diffControlContainer : undefined; const keySbsScroll = 'sync-diff-scroll'; if( eToggleParent ){ cbSync = D.checkbox(keySbsScroll, F.storage.getBool(keySbsScroll,true)); D.append(eToggleParent, D.append( D.addClass(D.create('span'), 'input-with-label'), D.append(D.create('label'), cbSync, "Scroll Sync") )); cbSync.addEventListener('change', function(e){ F.storage.set(keySbsScroll, e.target.checked); }); } const useSync = cbSync ? ()=>cbSync.checked : ()=>F.storage.getBool(keySbsScroll,true); const scrollLeft = function(event){ const table = this.parentElement.parentElement. parentElement.parentElement; if( useSync() ){ table.$txtPres.forEach((e)=>(e===this) ? 1 : (e.scrollLeft = this.scrollLeft)); } return false; }; const SCROLL_LEN = 64; const keycodes = Object.assign(Object.create(null),{ 37: -SCROLL_LEN, 39: SCROLL_LEN }); const handleDiffKeydown = function(e){ const len = keycodes[e.keyCode]; if( !len ) return false; if( useSync() ){ this.$txtPres[0].scrollLeft += len; }else if( this.$preCurrent ){ this.$preCurrent.scrollLeft += len; } return false; }; const initTableDiff = function f(diff, unifiedDiffs){ if(!diff){ let i, diffs; diffs = document.querySelectorAll('table.splitdiff'); for(i=0; ie.addEventListener('click', handleLRClick, false)); } } return this; } window.fossil.page.tweakSbsDiffs = function(){ document.querySelectorAll('table.splitdiff').forEach((e)=>initTableDiff(e)); }; initTableDiff(); }, false);