User:Mikevoir/betterDiff.js

$(function {

// Double load protection if (window.dev && window.dev.BetterDiff) {return;} (window.dev = window.dev || {}).BetterDiff = true; // Load dependencies and cache importArticles({       type: 'script',        articles: [ 'u:dev:MediaWiki:Modal.js' ]    }); var api = new mw.Api; var config = mw.config.get(['wgDiffNewId', 'wgAction', 'wgCanonicalSpecialPageName']); var tokens = { patrol: '', rollback: '' };	var can = { block: mw.config.get('wgUserGroups').some(function(group){return ['sysop', 'wiki-representative', 'soap'].includes(group);}), patrol: mw.config.get('wgUserGroups').some(function(group){return ['sysop', 'content-moderator'].includes(group);}), rollback: mw.config.get('wgUserGroups').some(function(group){return ['sysop', 'content-moderator', 'rollback'].includes(group);}) };	// Main class var betterDiff = { init: function { // Get tokens if (can.patrol) { api.get({					meta: 'tokens',					type: 'patrol'				}).then(function(data){					if (data.query.tokens.patroltoken && data.query.tokens.patroltoken.length>2) {						tokens.patrol = data.query.tokens.patroltoken;					}				}); }			// Check we're in Special:RecentChanges if (config.wgCanonicalSpecialPageName == 'Recentchanges') { betterDiff.waitFor('.mw-changeslist div', function{					betterDiff.newDiffLink;					betterDiff.quickDiff;					betterDiff.waitFor('.mw-rcfilters-ui-filterWrapperWidget-top', betterDiff.userPatrol);					// start observing					betterDiff.RecentChangesReload(betterDiff.newDiffLink);				}); }			// Check we're in Special:Contributions else if (config.wgCanonicalSpecialPageName == 'Contributions') { betterDiff.waitFor('ul.mw-contributions-list li', betterDiff.newDiffLink); }			// Check we're in a history page else if (config.wgAction == 'history') { betterDiff.waitFor('.mw-history-histlinks', betterDiff.newDiffLink); }			// Check we're in a diff page else if (config.wgDiffNewId) { betterDiff.waitFor('#mw-diff-ntitle1', betterDiff.newDiff); }		},		// Run callback every time Special:RecentChanges reloads results RecentChangesReload: function(callback, query) { var observer = new MutationObserver(function (mutations, me) {				if ( mutations[0] && mutations[0].target && mutations[0].target.classList.contains('mw-rcfilters-ui-changesListWrapperWidget') ) {					callback;				}			}); observer.observe(document.querySelector(query ? query : '.mw-changeslist'), {				childList: true,				subtree: true			}); },		// Properly build diff links with page creation edits newDiffLink: function { var newDiffLink = { searchRevid: function (row) { var O = { row: row, revid: [] };					// Special:Contributions if ( O.row.nodeName == 'LI' && O.row.getAttribute('data-mw-revid')) { O.revid.push(O.row.getAttribute('data-mw-revid')); // Page creation revision // Special:RecentChanges } else { // sole edit if (O.row.getAttribute('data-mw-revid')) { O.revid.push(O.row.getAttribute('data-mw-revid')); // Top edit } else if (O.row.classList.contains('mw-rcfilters-ui-highlights-enhanced-toplevel')) { O.revid.push(O.row.nextElementSibling.getAttribute('data-mw-revid')); // Last revision O.revid.push(O.row.parentElement.lastElementChild.getAttribute('data-mw-revid')); // Page creation revision }					}					if (O.revid.length>0) { return newDiffLink.buildLink(O); } else { return; }				},				buildLink: function(options) { var target = newDiffLink.getTarget(options.row); var page = newDiffLink.getTitle(options.row); if (target) { var link = document.createElement('a'); var from = options.revid[options.revid.length-1]; var to = options.revid[0]; var href = '/wiki/' + page + '?diff=' + to; if (from == to) { link.classList.add('mw-changeslist-diff'); } else { href = href + '&oldid=' + from; options.label = options.revid.length + ' changes'; link.classList.add('mw-changeslist-groupdiff'); }						link.href = href; link.title = page; if (target.nodeType == 3) { var split = /^([^\d\w]*)([\d\w\s]*)([^\d\w]*)$/.exec(target.textContent); link.innerHTML = split[2]; if (split[1].length>0) {target.parentNode.insertBefore(document.createTextNode(split[1]), target);} target.parentNode.insertBefore(link, target); if (split[3].length>0) {target.parentNode.insertBefore(document.createTextNode(split[3]), target);} target.remove; return; } else { link.innerHTML = options.label; target.replaceChildren(link); return; }					} else { return; } },				getTitle: function(row) { var title; var queries = [ '.mw-changeslist-title', // Normal Special:RecentChanges '.mw-changeslist-date', // Nested Special:RecentChanges '.mw-enhanced-rc-time > a' // Special:Contributions ];					queries.forEach(function(query) {						if (!title && row.querySelector(query) && row.querySelector(query).getAttribute('title')) {							title = row.querySelector(query).getAttribute('title');						}					}); return title || ''; },				getTarget: function(row) { var target; var queries = [ '.mw-changeslist-links:not(.mw-history-histlinks, .mw-usertoollinks) > span:first-child', // Special:Contributions && Normal Special:RecentChanges '.mw-changeslist-diff-cur + .mw-changeslist-separator', // Nested Special:RecentChanges '.mw-changeslist-links.mw-history-histlinks:not(.mw-usertoollinks) > span:last-child', // ?action=history ];					queries.forEach(function(query) {						var test = row.querySelector(query);						if (!target && test && test.classList.length==0) {							target = test.firstChild;						} else if (!target && test && test.classList.length>0 && test.classList.length>0) {							target = test.previousSibling;						}					}); return target; }			};			// Check we're in Special:RecentChanges if (config.wgCanonicalSpecialPageName == 'Recentchanges') { if (					document.querySelector('.mw-changeslist-src-mw-new') &&					( !document.querySelector('.mw-changeslist-src-mw-new .mw-changeslist-groupdiff') || !document.querySelector('.mw-changeslist-src-mw-new .mw-changeslist-diff') )				){					document.querySelectorAll('.mw-changeslist-src-mw-new').forEach(function(node){if (node) {newDiffLink.searchRevid(node);}}); }			}			// Check we're in Special:Contributions else if (config.wgCanonicalSpecialPageName == 'Contributions') { if (document.querySelector('ul.mw-contributions-list abbr.newpage')) { document.querySelectorAll('ul.mw-contributions-list abbr.newpage').forEach(function(node){						var row = node.parentElement;						if (row) {newDiffLink.searchRevid(row);}					}); }			}			// Check we're in a history page else if (config.wgAction == 'history') { if (!document.querySelector('ul.mw-contributions-list:last-of-type li:last-of-type .mw-history-histlinks > span:nth-child(2) > a')) { var node = document.querySelector('ul.mw-contributions-list:last-of-type li:last-of-type'); if (node) {newDiffLink.searchRevid(node);} }			}		},		// Properly display diff for page creation edits newDiff: function { if (document.querySelector('#mw-diff-ntitle1 > strong > a')) { var table = document.querySelector('.diff'); var revid = document.querySelector('#mw-diff-ntitle1 > strong > a').href.replace(/^.+\?oldid=/g, ''); var api_opt = { action: 'compare', torev: revid, fromslots: 'main', 'fromtext-main': '', prop: 'diff|ids', formatversion: 2 };				if ( revid && document.querySelector('#mw-diff-otitle1 > strong > a') && !document.querySelector('#differences-prevlink') ) { // Add edit link to page creation diff table.querySelector('#mw-diff-otitle4').innerHTML = ' strong > a').title +					'?diff=' +					document.querySelector('#mw-diff-otitle1 > strong > a').href.replace(/^.+\?oldid=/g, '') +					'&oldid=0" title="' +					document.querySelector('#mw-diff-otitle1 > strong > a').title +					'" id="differences-prevlink">← Older edit'; } else if (revid && !document.querySelector('#differences-prevlink')) { table.querySelector('.diff-ntitle').colSpan = 4; table.querySelector('.diff-notice').parentNode.remove; api.get(api_opt).then(function(data) {						table.innerHTML = '  ' + table.innerHTML;						table.querySelector('tbody').innerHTML = table.querySelector('tbody').innerHTML + data.compare.body;					}); }			}		},		// Mass patrol recent edits from specific user userPatrol: function { if (!document.querySelector('#userPatrol') && can.patrol) { var wrapper = $(					' '+						'User to mass patrol: '+						' '+						' '+						' '+						' Patrol User '+					' '				); $('.mw-rcfilters-ui-filterWrapperWidget-top').after(wrapper); document.querySelector('#submitUserPatrol').addEventListener('click', function(event) {					var user = document.querySelector('#userPatrol').value; // Username without the "User:" prefix					if (user.length>0) {						api.get({ action: 'query', list: 'recentchanges', rcshow: '!patrolled', rcprop: 'ids', rcuser: user, format: 'json', formatversion: '2', rclimit: 'max' }).then(function(data){ if (data.query.recentchanges.length>0) { document.querySelector('#userPatrolDetails').innerHTML = 'Patrolling '+data.query.recentchanges.length+' edits...'; var patrolID = function(page) { api.post({										action: 'patrol',										format: 'json',										revid: page.revid,										token: tokens.patrol									}); };								data.query.recentchanges.forEach(patrolID); document.querySelector('#userPatrolDetails').innerHTML = 'Patrolled '+data.query.recentchanges.length+' edits!'; } else { document.querySelector('#userPatrolDetails').innerHTML = 'User has no edits to patrol!'; }						});					} else { document.querySelector('#userPatrolDetails').innerHTML = 'No user specified.'; }				}); } else { console.log('User does not have patrolling rights.'); } },		// Diff pages but without moving away from the page, allowing patrolling still quickDiff: function { // Diff link string's storage for modal button var href = ''; // Add custom link var addQuick = function(diff) { if (diff && diff.getAttribute('href')) { var href = diff.getAttribute('href'); var newid = /diff=(\d+)/.exec(href)[1]; var oldid = /oldid=\d+/.test(href) ? /oldid=(\d+)/.exec(href)[1] : '0'; var link = document.createElement('a'); link.setAttribute('newid', newid); link.setAttribute('oldid', oldid); link.setAttribute('data-target-page', diff.closest('table').querySelector('a.mw-changeslist-title').getAttribute('title')); link.innerHTML = 'view'; link.style.cursor = 'pointer'; link.classList.add('quickDiff'); if (diff.parentElement.nodeName == 'SPAN') { var span = document.createElement('span'); span.appendChild(link); diff.parentElement.after(span); } else { diff.after(' | ', link); }				}			};			// Get locations where to add custom link var addLinks = function { if (!document.querySelector('.quickDiff')) { document.querySelectorAll('.mw-changeslist-groupdiff').forEach(addQuick); document.querySelectorAll('.mw-changeslist-diff').forEach(addQuick); }			};			addLinks; betterDiff.RecentChangesReload(addLinks); // Build modal and start up listeners mw.hook('dev.modal').add(function(Modal) {				var popup = 					new Modal.Modal({ title: 'Quick Diff', id: 'quickDiff-popup', size: 'full', content: '', buttons: [ {								text:'Copy Diff Link', title:'Copy Diff Link', id:'quickDiff-CopyDiffLink', primary: true, event: 'CopyDiffLink' }						],						events: { CopyDiffLink: function { navigator.clipboard.writeText(href); }						}					});				popup.create;				document.addEventListener('click', function(event) { // Load diff modal if (event.target && event.target.classList.contains('quickDiff')) { if (tokens.rollback=='' && document.querySelector('.mw-rollback-link a')) { tokens.rollback = document.querySelector('.mw-rollback-link a').href.replace(/^.+token=/, ''); }						var generateHeader = function(data) { var header = ''; if (data.fromtimestamp) { var todate = new Date(data.totimestamp); var fromdate = new Date(data.fromtimestamp); href = mw.config.get('wgServer')+'/wiki/'+betterDiff.urlEncode(data.totitle)+'?diff='+data.torevid+'&oldid='+data.fromrevid; header += // Old revid ''+ ' '+										' '+											''+ 'Revision as of '+ todate.getHours+':'+todate.getMinutes+ ', '+todate.getDate+' '+ (new Intl.DateTimeFormat('en-US', {month: 'long'}).format(todate))+' '+ todate.getFullYear+ ' '+ ' ('+												'edit'+											') '+ ' '+									' '+									' '+										' '+data.fromuser+'  '+ ' ('+											'wall</a> | '+											'contribs</a>'+										') '+ ' '+									' '+										' '+											(data.fromparsedcomment ? ('('+data.fromparsedcomment+')') : '')+ ' '+									' '+								' '+								// New revid '<td class="diff-ntitle diff-side-added" colspan="2">'+ ' '+										' '+											''+ 'Revision as of '+ todate.getHours+':'+todate.getMinutes+ ', '+todate.getDate+' '+ (new Intl.DateTimeFormat('en-US', {month: 'long'}).format(todate))+' '+ todate.getFullYear+ '</a> '+ ' ('+												'edit</a>'+											') '+ ' ('+												'undo</a>'+											') '+ ' '+									' '+									' '+										' '+data.touser+' </a> '+ ' ('+											'wall</a> | '+											'contribs</a>'+										') '+ (tokens.rollback.length>2 ? ( ' '+												''+													'rollback'+ '</a>'+ ' '										) : )+									' '+									' '+										' '+											(data.toparsedcomment ? ('('+data.toparsedcomment+')') : )+ ' '+									' '+									' '+								' ';							} else { var date = new Date(data.totimestamp); href = mw.config.get('wgServer')+'/wiki/'+betterDiff.urlEncode(data.totitle)+'?diff='+data.torevid; header += '<td class="diff-ntitle" colspan="4">'+ ' '+										' '+											''+ 'Revision as of '+ date.getHours+':'+date.getMinutes+ ', '+date.getDate+' '+ (new Intl.DateTimeFormat('en-US', {month: 'long'}).format(date))+' '+ date.getFullYear+ '</a>'+ ' ('+												'edit</a>'+											') '+ ' '+									' '+									' '+										' '+data.touser+' </a> '+ ' ('+											'wall</a> | '+											'contribs</a>'+										') '+ (tokens.rollback.length>2 ? ( ' '+												''+													'rollback'+ '</a>'+ ' '										) : )+									' '+									' '+										' '+											(data.toparsedcomment ? ('('+data.toparsedcomment+')') : )+ ' '+									' '+									' '+								' ';							}							return header; };						var api_opt = { action: 'compare', torev: event.target.getAttribute('newid'), prop: 'diff|ids|timestamp|user|comment|title', formatversion: 2 };						if (event.target.getAttribute('oldid') !== '0') { api_opt.fromrev = event.target.getAttribute('oldid'); } else { api_opt.fromslots = 'main'; api_opt['fromtext-main'] = ''; }						api.get(api_opt).then(function(data) {							popup.setTitle('Changes: '+data.compare.totitle);							popup.setContent( ' '							);							if (can.patrol && tokens.patrol.length>2) {								// Add patrol button if any revision to patrol								api.get({ action: 'query', list: 'recentchanges', rcshow: '!patrolled', rcprop: 'ids', format: 'json', rctitle: data.compare.totitle, formatversion: '2', rclimit: 'max' }).then(function(check) { var num = 0; while (check.query.recentchanges[num]) { if (											(check.query.recentchanges[num] && data.compare.torevid && data.compare.fromrevid && check.query.recentchanges[num].revid <= data.compare.torevid && check.query.recentchanges[num].revid >= data.compare.fromrevid) || 											(check.query.recentchanges[num] && data.compare.torevid && !data.compare.fromrevid && check.query.recentchanges[num].revid == data.compare.torevid)										) { document.querySelector('#mw-diff-ntitle4').innerHTML = ' ['+													''+														'Mass Patrol'+ '</a>'+ '] ';											num = -1; } else { num++; } }								}).catch(console.log);							}							popup.show;						}); // Patrol revisions shown in modal if user has perms and there's any to patrol } else if (event.target && event.target.nodeName == 'A' && event.target.closest('.patrollink') && event.target.getAttribute('torevid')) { document.querySelector('.patrollink a').outerHTML = '<img src="https://www.superiorlawncareusa.com/wp-content/uploads/2020/05/loading-gif-png-5.gif" width="25px" style="vertical-align: baseline;" border="0" />'; var torevid = event.target.getAttribute('torevid'); var fromrevid = event.target.getAttribute('fromrevid'); api.get({							action: 'query',							list: 'recentchanges',							rcshow: '!patrolled',							rcprop: 'ids',							format: 'json',							rctitle: event.target.getAttribute('title'),							formatversion: '2',							rclimit: 'max'						}).then(function(data) {							var num = 0;							var revids = [];							while ( data.query.recentchanges[num] && (									(fromrevid && torevid && data.query.recentchanges[num].revid >= fromrevid && data.query.recentchanges[num].revid <= torevid) ||									(torevid && data.query.recentchanges[num].revid == torevid)								) ) {								revids.push(data.query.recentchanges[num].revid);								num++;							}							if (revids.length>0) {								revids.forEach(betterDiff.patrolRevision);								document.querySelector('.patrollink a').outerHTML = 'Edits patrolled: '+revids.length;							} else {								document.querySelector('.patrollink a').outerHTML = 'Error, no valid revisions found!';								console.log('api result:',data);							}						}).catch(function(err){							document.querySelector('.patrollink a').outerHTML = 'API error, please contact Mikevoir</a>!';							console.log('api result:', err);						}); }				});			});		},		// Patrol inputted revid if user can patrol patrolRevision: function(id) { if (can.patrol && id && tokens.patrol) { api.post({					action: 'patrol',					format: 'json',					revid: id,					token: tokens.patrol				}).catch(console.log); }		},		// Encode url with MediaWiki sensitive characters only urlEncode: function(url) { url = url || ''; url = url .replace(' ', '_') .replace(/\?/, '%3F') .replace(/\&/, '%26'); return url; },		// Delay until element exists to run function waitFor: function(query, callback, extraDelay) { if ('function' == typeof callback && 'string' == typeof query) { extraDelay = extraDelay || 0; if (document.querySelector(query)) { setTimeout(callback, extraDelay); } else { // set up the mutation observer var observer = new MutationObserver(function (mutations, me) {						// mutations is an array of mutations that occurred						// me is the MutationObserver instance						var targetNode = document.querySelector(query);						if (targetNode) {							setTimeout(callback, extraDelay);							me.disconnect; // stop observing							return;						}					}); // start observing observer.observe(document, {					 childList: true,					  subtree: true					}); }			}		}	};	// Load styles and start when API is loaded mw.loader.using('mediawiki.diff.styles'); mw.loader.using('mediawiki.api').then(betterDiff.init); });