From Wikipedia, the free encyclopedia
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 } );

} () );