Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/* <nowiki>
AnonLoader, allows anons to load scripts specified in window.AnonLoaderScripts (which can be defined in MediaWiki:common.js) through portlet links
AnonLoader is public domain, irrevocably released as WTFPL Version 2[www.wtfpl.net/about/] by its author, Alexis Jazz.
Installation instructions:
* Create MediaWiki:Gadget-AnonLoader.js with the contents of this file.
* Add this line to MediaWiki:Gadgets-definition: "AnonLoader[ResourceLoader|dependencies=mediawiki.util,mediawiki.storage|targets=desktop,mobile|default|hidden]|AnonLoader.js"
* Put (for example) window.AnonLoaderScripts = ['CD','Factotum:gadget','dark-mode-toggle,dark-mode-toggle-pagestyles:gadget:Dark mode','dark-mode:gadget:Instant dark mode:urlLoad']; in MediaWiki:Common.js AND MediaWiki:Mobile.js (MobileFrontend doesn't load Common.js)
* Optional: add window.AnonLoaderShowAll=1 in common.js+mobile.js to show all gadgets without a collapsible menu.
AnonLoaderScripts format description:
Gadgets:
['GADGET1:gadget[:displayname][:urlLoad]','GADGET2[,GADGET3][,GADGET4]:gadget[:displayname]','GADGET5:gadget[:displayname]']
Only one gadget can have urlLoad. This will intercept links to add &withgadget=gadgetname. Dark mode requires this to prevent white flashes.
To load multiple gadgets for one entry, enter them separated by commas.
Scripts (that can't be loaded as gadgets):
['SCRIPTNAME1[:0][:displayname]','SCRIPTNAME2[:0][:displayname]','SCRIPTNAME3[:0][:displayname]']
If :gadget is added (HIGHLY RECOMMENDED) it will load ext.gadget.SCRIPTNAME, e.g. https://commons.wikimedia.beta.wmflabs.org/w/load.php?lang=en&modules=ext.gadget.AnonLoader
Otherwise it will load MediaWiki:Gadget-SCRIPTNAME.js. Insecure page titles are not supported.
If you include displayname that will be used for the link name.
Technically anons could modify their localStorage to load scripts/gadgets that aren't specified in AnonLoaderScripts, but they would still have to comply with the requirements of being a gadget or titled MediaWiki:Gadget-SCRIPTNAME.
There's no UI for that, but very meta, one could write a gadget for that and load it with AnonLoader.
*/
/*globals mw:false,$:false*/
window.AnonL = {};
var AnonL = window.AnonL;
AnonL.L = function(name,u1,u2,u3,u4,p,p2,p3,e,ls,lsk,s,s2,i,i2,i3,a,el,eln,dir,g,n,le,c,mobile,pID,aID,dN,cg,sa,gpv) {
'use strict';
gpv = function(k){
	return mw.util.getParamValue(k);
};
cg = function(k){
	return mw.config.get(k);	
};
if (cg('wgUserName')){
	return;//only anons need this as they don't have Special:Preferences
}
if (Array.from(document.getElementsByTagName('body')[0].classList).indexOf('rtl') != -1){
	dir='right';
} else {
	dir='left';
}
a = (window.AnonLoaderScripts||[]);
sa = gpv('AnonLoaderShowAll') || window.AnonLoaderShowAll; //ShowAll, no menu that toggles the gadget list, just show all
mobile = cg('skin') == 'minerva';
le = '.AnonLMin,.AnonLPlus';
c = 'AnonLNoD';
p = gpv('ALSet');
p2 = gpv('ALRemove');
p3 = Number(mw.storage.get('AnonLCheck')||0);
dN = new Date().getTime();
ls = JSON.parse((mw.storage.get('AnonLoader')||'{}'));
if ( p3+30000 > dN ) { //link was clicked <30s ago
	if ( p ) {
		ls[p.split(/:/)[0]] = p;
	}
	if ( p2 ) {
		delete ls[p2.split(':')[0]];
	}
}
mw.storage.remove('AnonLCheck');
lsk = Object.keys(ls);
for(i3=0;i3<lsk.length;i3++){
	if ( a.indexOf(ls[lsk[i3]]) == -1 ) {
		delete ls[lsk[i3]]; //remove entries from localStorage that don't match window.AnonLoaderScripts
		lsk = Object.keys(ls);
	}
}
mw.storage.set('AnonLoader',JSON.stringify(ls));
g=[];
AnonL.fixUrl = function(event,targ,uri){
	targ = event.delegateTarget;
	try{
		uri = new mw.Uri(targ.href);
		if ( uri.host == cg('wgServerName') ) {
			uri.extend({withgadget:AnonL.gdgt});
			targ.href = uri.toString();
			event.delegateTarget.href = uri.toString();
		}
	} catch (ev) {}
};
AnonL.fixU = function(el,skip) {
	$(el).on('click mousedown focus',AnonL.fixUrl);
	if ( !skip ) {
		$(el).addClass('ALwgdgt');
	}
};
/* test scenarios (in all skins+mobile from the search box and Special:Search):
* Enter search term, click ajax result
* Enter search term that matches a title, press enter (goes to article)
* Enter search term that doesn't, press enter (goes to results)
* Skins: cologneblue, modern, timeless, minerva, monobook, vector, vector-2022
* dark-mode does not support cologneblue
*/
AnonL.rS = function(s,i2) { //restore search api.php action
	for (i2=0;i2<$(s).length;i2++) {
		$(s)[i2].attributes.action.value = cg('wgScript');
	}
};
AnonL.fixSearch = function(els,i,i2,u,uri,t,nl,s,sv,me) {
	els = $('.suggestions-results a,.skin-minerva .results-list-container a');
	s = 'form#searchform,.skin-minerva form.search-box';
	if ( !els.length ) {
		AnonL.rS(s);
	} else {
		sv = $('input#searchInput')[0].value;
		me = '.skin-minerva .search-overlay .search-box input.search';
		if ( mobile && sv == '' && $(me)[0] ) {
			sv = $(me)[0].value;
		}
	}
	for (i=0;i<els.length;i++) {
		u = els[i];
		try{
			uri = new mw.Uri(u.href);
			t = uri.query.search;
			if ( t ) {
				nl = cg('wgArticlePath').replace('$1',t);
				if ( i == 0 && sv.toLowerCase() == t.toLowerCase() ) { //entered search term matches first result, you'd get redirected there anyway, I'll save you the trip
					for (i2=0;i2<$(s).length;i2++) {
						$(s)[i2].attributes.action.value = nl;
					}
				} else if ( i == 0 ) {
					AnonL.rS(s);
				}
				u.href = cg('wgServer')+nl;
			} else if ( cg('wgArticlePath').replace('$1',sv).toLowerCase() == uri.path.toLowerCase() ) {
				//adjust search form target
				for (i2=0;i2<$(s).length;i2++) {
					$(s)[i2].attributes.action.value = uri.path;
					if ( !$(s)[i2].classList.contains('ALwgdgt') ) { //add hidden input to minerva overlay search
						$(s)[i2].append(AnonL.hEl[0]);
						$(s)[i2].classList.add('ALwgdgt');
					}
				}
			}
			$(u).on('click mousedown focus',AnonL.fixUrl);
		} catch (ev) {}
	}
};
AnonL.uLoad = function(fl,fEl,hEl,i,sEls,sElUrls,s){
	AnonL.observe = function(rec,newEl,i,i2,nn) {
		AnonL.fixSearch();
		//AnonL.fixU('.suggestions-results a:not(.ALwgdgt)');
/*
		newEl = rec[0].addedNodes;
		for (i=0;i<newEl.length;i++){
			try{
				nn = newEl[i].nodeName;
				if ( nn == 'A' ) {
					AnonL.fixU(newEl[i]);
				} else if ( nn != '#text' ) {
					AnonL.fixU(newEl[i].querySelectorAll('a:not(.ALwgdgt)'));
				}
			} catch (ev) {}
		}
*/
	};
	AnonL.observer = new MutationObserver(AnonL.observe);
	AnonL.observer.observe(document.body,{childList: true,subtree: true,attributes: false,characterData: false}); //MediaWiki magically creates elements in some cases (like basic search turning into something else on click)
/*
	sEls = 'input:not(.ALwgdgt),.results-list-container:not(.ALwgdgt)';
	sElUrls = '.results-list-container a:not(.ALwgdgt)';
	AnonL.hrefFix = function() {
		$(sEls).on('click touchend keydown',AnonL.hrefFix); //spread
		$(sEls).addClass('ALwgdgt');
		AnonL.fixU(sElUrls);
	};
	$(sEls).on('click touchend keydown',AnonL.hrefFix);
*/
	AnonL.fixU('a',1);
	fEl = 'form:not(.ALwgdgt)'; //adding hidden input to forms so withgadget will persist when they are submitted
	hEl = {};
	$('body,input').on('click touchend',function(event,targ,uri){
		for(i=0;i<$(fEl).length;i++){
			if ( $(fEl)[i].attributes.action.value.match(/^\/[^\/]/) ) {
				hEl[i] = document.createElement('input');
				hEl[i].type = 'hidden';
				hEl[i].name = 'withgadget';
				hEl[i].value = AnonL.gdgt;
				AnonL.hEl = hEl;
				$(fEl)[i].append(hEl[i]);
				$(fEl)[i].classList.add('ALwgdgt');
			}
		}
	});
	s = 'input#searchInput:not(.ALwgdgt),.skin-minerva .search-box input.search:not(.ALwgdgt)';
	$(s).on('click touchend change',AnonL.fixSearch);
	$(s).addClass('ALwgdgt');
};
for (i=0;i<lsk.length;i++){
	s = ls[lsk[i]].split(':');
	if (s[1]=='gadget') {
		s2 = s[0].split(',');
		for (i2=0;i2<s2.length;i2++){
			g.push('ext.gadget.'+s2[i2]);	//collect gadget
		}
		if ( s[3] == 'urlLoad' ) { //there can be only one gadget loaded this way it seems
			AnonL.gdgt = s2[0];
			AnonL.uLoad();
		}
	} else {
		mw.loader.load(cg('wgScriptPath')+'/index.php?title=MediaWiki:gadget-'+s[0]+'.js&action=raw&ctype=text/javascript');
	}
}
mw.loader.load(g);//load gadgets
el={};
eln={};
if ( mobile ) {
	pID = 'p-navigation';
	aID = '';
} else {
	pID = 'p-tb';
	aID = '#t-specialpages';
}
if ( !sa ) {
	el.menu = mw.util.addPortletLink(
		pID,
		'',
		AnonL.lang['prefs-gadgets'],
		'AnonLoaderMenu',
		AnonL.lang['prefs-gadgets'],
		undefined,
		aID
	);
	if ( mobile ) {
		$('.mw-ui-icon-portletlink-AnonLoaderMenu').addClass('mw-ui-icon-minerva-die');
	}
}
$('#AnonLoaderMenu,#AnonLoaderMenu a').on('click',function(event){
	event.stopPropagation();event.preventDefault();
	if ( $(le).hasClass(c) ) {
		$(le).removeClass(c);
	} else {
		$(le).addClass(c);
	}
});
for (i=0;i<a.length;i++){
	if ( ls[a[i].split(':')[0]] ) {
		u1='Remove';
		u3='AnonLMin';
	} else {
		u1='Set';
		u3='AnonLPlus';
	}
	n = a[i].split(':');
	u4 = '';
	if ( n[3] == 'urlLoad' && u1 == 'Set' ) {
		u4 = '&withgadget='+n[0];
	}
	eln[i] = (n[2] || n[0]);
	if ( sa && u1 == 'Set' ) {
		eln[i] = AnonL.lang['AnonLoader-enable'].replace('$1',eln[i]);
	} else if ( sa && u1 == 'Remove' ) {
		eln[i] = AnonL.lang['AnonLoader-disable'].replace('$1',eln[i]);
	}
	el[i] = mw.util.addPortletLink(
	pID,
	'?AL'+u1+'='+a[i]+u4,
	eln[i],
	'AnonLoader'+i,
	eln[i],
	undefined,
	aID
	);
	$('body.skin-minerva #'+'AnonLoader'+i+' .mw-ui-icon').addClass(c);
	if ( mobile ) {
		document.getElementById('AnonLoader'+i.toString()).querySelectorAll('a')[0].classList.add(u3);
	} else {
		document.getElementById('AnonLoader'+i.toString()).classList.add(u3);
	}
}
$(le).addClass(c);
$(le).on('click',function(){
	mw.storage.set('AnonLCheck',new Date().getTime());
});
if ( !sa ) {
	mw.util.addCSS('.AnonLNoD{display:none !important}.AnonLMin:before,.AnonLPlus:before{margin-'+dir+':0.5em;display:inline-block;text-align:center;width:1em;font-size:larger;font-weight:bold}.AnonLMin:before{content:"✔";color:#060}.AnonLPlus:before{content:"✘";color:#600}');
}
};
AnonL.loadLang = function(l,ls,d,e) {
	l = mw.config.get('wgUserLanguage');
	ls = JSON.parse((mw.storage.get('AnonLoaderLang2')||'{}'));
	e = 'Enable $1';
	d = 'Disable $1';
	ls.en = {'prefs-gadgets':'Gadgets','AnonLoader-enable':e,'AnonLoader-disable':d};
	if ( ls[l] ) {
		AnonL.wait(ls[l]);
	} else {
		mw.loader.using(['mediawiki.api'], function(){
			var api = new mw.Api();
			api.get( {action:'query',meta:'allmessages',ammessages:['prefs-gadgets','AnonLoader-enable','AnonLoader-disable'],amlang:l}).done( function ( data,dq ) {
				dq = data.query.allmessages;
				ls[l] = {'prefs-gadgets':dq[0]['*'],'AnonLoader-enable':(dq[1]['*']||e),'AnonLoader-disable':(dq[2]['*']||d)};
				mw.storage.set('AnonLoaderLang2',JSON.stringify(ls));
				AnonL.wait(ls[l]);
			});
		});
	}
};
AnonL.wait = function(g,i) {
	AnonL.lang = g;
	i=0;
	var AnonW = setInterval(function(){ //wait for the presence of window.AnonLoaderScripts
		i++;
		if (window.AnonLoaderScripts || i > 50) { //test 10 times a second for 5 seconds
			clearInterval(AnonW);
			mw.loader.using(['mediawiki.Uri','mediawiki.util'], function(){
				AnonL.L();
			});
		}
	}, 100);
};
AnonL.loadLang();
//</nowiki>