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.
/*********************************************************************************************************************************
 * Currently still in development, this is designed to provide a custom list of Quick Links to Wikipedia pages.
 * If you encounter any problems using this script, please tell User:Fred_Gandt on either my talk page or this script's talk page.
 *
 *********************************************************************************************************************************/
/* TODO: Handle #sections */
/* TODO: Reduce API calls */

( function() {
	"use strict";
	var eByTn = function( p, n, i, nl ) { nl = p.getElementsByTagName( n ); return i !== undefined ? nl[ i ] : nl; },
		eById = function( id ) { return document.getElementById( id ); },
		cE = function( e ) { return document.createElement( e ); },
		nl2a = function( nl ) { return [].slice.call( nl ); },
		
		WG_pagename = mw.config.get( "wgPageName" ),
		BASE = "fg-quick-links",
		EXT = BASE + "-",
		SWITCH = EXT + "switch",
		VIEW = EXT + "view",
		EMPTY = EXT + "empty", 
		OPEN = EXT + "open",
		TITLE = EXT + "title",
		STORAGE = EXT + "storage",
		QL = EXT + "ql",
		QLE,
		NPT,
		
		namespace = l => /^(?:([^\:]+)\:)?(.+)$/.exec( l ),
		
		toggleBase = e => ( e || ql.ui ).classList.toggle( BASE ),
		
		underspace = ( s, b ) => b ? s.replace( /_/g, " " ) : s.replace( / /g, "_" ),
		
		gotIt = v => ql.ui.querySelector( 'a[title="' + underspace( v || WG_pagename, true ).replace( /\"/g, "\\\"" ) + '"]' ),
		
		ql = {
			optnnm: { local: EXT + mw.config.get( "wgUserName" ).replace( / /g, "-" ), global: "userjs-" + BASE },
			optnvlu: [ { "Mainspace": [] }, { "Mainspace talk": [] } ],
			alss: { undefined: "Mainspace", "talk": "Mainspace talk" },
			ui: cE( "li" )
		},
		
		initOptionValue = function() {
			var fns = mw.config.get( "wgFormattedNamespaces" ),
				nsi = mw.config.get( "wgNamespaceIds" ),
				ns, cns, cnsi, tmp;
			for ( ns in nsi ) {
				cnsi = nsi[ ns ];
				cns = fns[ cnsi ];
				if ( underspace( cns ).toLowerCase() === ns && cnsi !== 0 && cnsi !== 1 ) {
					tmp = {};
					tmp[ cns ] = [];
					ql.optnvlu.push( tmp );
				} else {
					ql.alss[ ns ] = cns;
				}
			}
			return ql.optnvlu;
		},
		
		linkify = function( v, d ) {
			v = v.replace( /^Mainspace(?:[ _]{1}talk)?\:/i, "" );
			var u = underspace( v, true );
			if ( d ) {
				u = u.replace( /[\.\%]{1}(2[1-9a-c]{1}|[357][b-e]{1}|[23]f|[46]0|c2[\.\%]{1}a([01]{1}))/gi, function( m, g1, g2 ) {
					if ( g2 ) {
						return { "0": " ", "1": "¡" }[ g2.toLowerCase() ];
					}
					return { "21": "!", "22": """, "23": "#", "24": "$", "25": "%", "26": "&", "27": "'", "28": "(", "29": ")", "2a": "*", "2b": "+", "2c": ",", "2f": "/",
						"3b": ";", "3c": "<", "3d": "=", "3e": ">", "3f": "?", "5b": "[", "5c": "\", "5d": "]", "5e": "^", "7b": "{", "7c": "|", "7d": "}", "7e": "~",
						"40": "@", "60": "`" }[ g1.toLowerCase() ];
				} );
			}
			return '<a href="/wiki/' + mw.util.wikiUrlencode( v ) + '" title="' + u.replace( /\"/g, "&quot;" ) + '">' + namespace( u )[ 2 ] + '</a>';
		},
		
		quickLinks = function() {
			var vlus = ql.optnvlu, o = [], vlu, qls, on, oa, ok,
				iterate = function( a ) {
					var i = [], v;
					for ( v in a ) {
						i.push( linkify( a[ v ] ) );
					}
					return i.join( '</li><li>' );
				},
				filler = function( a ) {
					if ( a.length ) {
						return '<li>' + iterate( a ) + '</li>';
					}
					return "";
				},
				brynner = function( t, c, f ) {
					var u = cE( "ul" );
					u.id = underspace( EXT + t );
					if ( c ) {
						u.setAttribute( "class", c );
					}
					u.innerHTML = '<li class="' + TITLE + '">' + t + '</li>' + f;
					return u.outerHTML;
				};
			for ( vlu in vlus ) {
				qls = vlus[ vlu ];
				ok = Object.keys( qls );
				on = ok[ 0 ];
				oa = qls[ on ];
				o.push( brynner( on, !oa.length ? EMPTY : ( ok[ 1 ] ? OPEN : false ), filler( oa ) ) );
			}
			return o.join( "" );
		},
		
		switchSwitch = function( t ) {
			var s = eById( SWITCH );
			if ( t ) {
				toggleBase( s );
			} else {
				s.classList.toggle( BASE, gotIt() );
			}
		},
		
		save = function( ss ) {
			var uls = nl2a( eByTn( ql.ui, "ul" ) ), tmpoptnvlu = [],
				ul, la, tmp, cul,
				titleArray = function( a ) {
					var l, ta = [];
					for ( l in a ) {
						ta.push( underspace( eByTn( a[ l ], "a", 0 ).title ) );
					}
					return ta.sort();
				},
				showError = function( e ) {
					alert( "Something went wrong:\n\n" + e );
				};
			for ( ul in uls ) {
				tmp = {};
				cul = uls[ ul ];
				la = nl2a( eByTn( cul, "li" ) );
				tmp[ la[ 0 ].textContent ] = titleArray( la.slice( 1 ) );
				if ( cul.classList.contains( OPEN ) ) {
					tmp.open = true;
				}
				tmpoptnvlu.push( tmp );
			}
			$.ajax( {
				type: "POST",
				url: "/w/api.php",
				dataType: "json",
				data: {
					format: "json",
					action: "options",
					token: mw.user.tokens.values.csrfToken,
					optionname: ql.optnnm.global,
					optionvalue: JSON.stringify( tmpoptnvlu )
				},
				success: function( data ) {
					if ( !data.error ) {
						localStorage[ STORAGE ] = JSON.stringify( QLE.innerHTML );
						ql.optnvlu = tmpoptnvlu;
						switchSwitch( ss );
					} else {
						QLE.innerHTML = quickLinks();
						showError( data.error.info );
					}
				},
				error: function( something, went, wrong ) {
					QLE.innerHTML = quickLinks();
					console.error( something );
					showError( went + ":\n\n" + wrong );
				}
			} );
		},
		
		addThis = function( v, d ) {
			var alias = function( q ) {
					return ql.alss[ q ? q.toLowerCase() : q ] || q;
				},
				li, ul = eById( EXT + underspace( alias( namespace( v )[ 1 ] ) ) );
			if ( ul ) {
				li = cE( "li" );
				li.innerHTML = linkify( v, d );
				ul.appendChild( li );
				ul.classList.remove( EMPTY );
				return li;
			}
			return false;
		},
		
		removeThis = function( t ) {
			var tp = t.parentElement, tpp = tp.parentElement;
			tpp.removeChild( tp );
			tpp.classList.toggle( EMPTY, nl2a( eByTn( tpp, "li" ) ).length < 2 );
		},
		
		setListeners = function() {
			var prepText = function( txt ) {
					return ( /(?:^.*w(?:iki)?\/(?:.+title\=)?)?([^&]+)/ ).exec( txt.trim() )[ 1 ];
				},
				processText = function( vlu, d ) {
					if ( vlu && !gotIt( vlu ) ) {
						if ( addThis( underspace( vlu ), d ) ) {
							save();
							NPT.value = "";
						} else if ( !confirm( "Something about that value isn't correct.\nModify it and try again?" ) ) {
							NPT.value = "";
						}
					}
				};
				
			ql.ui.addEventListener( "click", evt => {
				var trg = evt.target, nn = trg.nodeName.toLowerCase(), ths = gotIt();
				if ( nn === "button" ) {
					toggleBase();
				} else if ( nn === "a" ) {
					if ( trg.id === SWITCH ) {
						evt.preventDefault();
						if ( !ths ) {
							addThis( WG_pagename );
						} else {
							removeThis( ths );
						}
						save( true );
					} else if ( trg.id === VIEW ) {
						evt.preventDefault();
						toggleBase();
					}
				} else if ( nn === "li" ) {
					if ( !trg.classList.contains( TITLE ) ) {
						removeThis( eByTn( trg, "a", 0 ) );
					} else {
						trg.parentElement.classList.toggle( OPEN );
					}
					save();
				}
			}, false );
			ql.ui.addEventListener( "dragover", evt => evt.preventDefault() );
			ql.ui.addEventListener( "drop", evt => {
				evt.preventDefault();
				processText( prepText( evt.dataTransfer.getData( "text" ) ), true );
			} );
			NPT.addEventListener( "paste", evt => {
				evt.preventDefault();
				processText( prepText( evt.clipboardData.getData( "text" ) ), true );
			}, false );
			NPT.addEventListener( "change", evt => processText( NPT.value ) );
			window.addEventListener( "storage", evt => {
				var k = evt.key, nv = evt.newValue;
				if ( k && k === STORAGE && nv ) {
					QLE.innerHTML = JSON.parse( nv );
					switchSwitch();
					delete localStorage[ STORAGE ];
				}
			}, false );
		};
		
	ql.optnvlu = JSON.parse( mw.user.options.values[ ql.optnnm.global ] || JSON.stringify( initOptionValue() ) );
	$( document ).ready( () => {
		ql.ui.id = BASE;
		ql.ui.innerHTML = `<span><a id="${SWITCH}" href="#"></a><span><a id="${VIEW}" href="#"></a><div><input type="text" placeholder="Add a new link"><div id="${QL}">${quickLinks()}</div><button>Close</button></div></span></span>`;
		
		const STYLE_SHEET = new CSSStyleSheet();
		document.adoptedStyleSheets = [ ...document.adoptedStyleSheets, STYLE_SHEET ];
		STYLE_SHEET.replaceSync( `#fg-quick-links-switch {
	text-decoration: none;
	padding: .5em .2em;
	font-size: 1.7em;
	background: none;
	height: 1.46em;
	color: #ffbc41;
	width: 1em;
}
#fg-quick-links-switch::before {
	content: "☆";
}
#fg-quick-links-switch.fg-quick-links::before {
	content: "★";
}
#fg-quick-links-view {
	text-decoration: none;
	padding: .8em .3em;
	background: none;
	font-size: 1.1em;
	height: 2.3em;
	color: unset;
	opacity: .5;
	width: 2em;
}
#fg-quick-links-view::before {
	content: "🔍";
}
#fg-quick-links span > span {
	display: inline;
}
#fg-quick-links span > div {
	display: none;
	position: absolute;
	min-width: 300px;
	background: #fff;
	z-index: 2000;
	margin-top: 2.2em;
	padding: 1em;
	border: 1px solid #a7d7f9;
	border-radius: 3px;
	box-shadow: 2px 2px 15px -2px rgba(0, 0, 0, 0.5);
}
#fg-quick-links-ql {
	max-height: calc( 80vh - 13em );
    padding-right: 2em;
	overflow: auto;
	overflow-x: hidden;
	overscroll-behavior: contain;
}
#fg-quick-links-ql ul {
	float: none !important;
	background: none;
}
#fg-quick-links-ql ul.fg-quick-links-empty {
	display: none;
}
#fg-quick-links-ql li {
	float: none !important;
	height: auto;
	background: none;
}
#fg-quick-links-ql li.fg-quick-links-title {
	font-weight: bold;
	color: #666;
	cursor: pointer;
}
#fg-quick-links-ql li:not( .fg-quick-links-title ) {
	display: none;
    margin-left: 1.3em;
}
#fg-quick-links-ql li a {
	padding: 0;
	float: none;
	height: auto;
	display: block;
	margin-left: 1.3em;
	background-image: none;
}
#fg-quick-links-ql ul li.fg-quick-links-title::before {
	content: "► ";
	float: left;
	color: #aaa;
}
#fg-quick-links-ql ul.fg-quick-links-open li.fg-quick-links-title::before {
	content: "▼ ";
}
#fg-quick-links-ql li:not( [class=fg-quick-links-title] )::before {
	content: "x";
	float: left;
	color: #fff;
	background: rgba( 255, 0, 0, 0.5 );
	border-radius: 100%;
	padding: 1px 3px;
	font-size: 10px;
	line-height: 10px;
	margin-top: 2px;
	cursor: pointer;
}
#fg-quick-links input {
	margin-bottom: 1em;
	width: calc( 100% - 2em - 2px );
	padding: 0.5em 1em 0.6em;
	border: 1px solid #aaa;
	border-radius: 3px;
}
#fg-quick-links button {
	display: none;
	margin-top: 1em;
}
#p-views,
#fg-quick-links span:hover > div,
#fg-quick-links.fg-quick-links button,
#fg-quick-links.fg-quick-links span > div,
#fg-quick-links-ql ul.fg-quick-links-open li {
	display: block;
}` );
		eByTn( eById( "p-views" ), "ul", 0 ).append( ql.ui );
		NPT = eByTn( ql.ui, "input", 0 );
		QLE = eById( QL );
		switchSwitch();
		setListeners();
	}, { once: true } );
} () );