User:Fred Gandt/userResourceManager.js

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 control over user JavaScripts and StyleSheets.
 * If you encounter any problems using this script, please tell User:Fred_Gandt on either my talk page or this script's talk page.
 *
 *********************************************************************************************************************************/

( function( DOM_d ) {
	"use strict";
	var BASE = "fg-js-and-css-manager",
		EXT = BASE + "-",
		DROPEE_BOTTOM = EXT + "dropee-bottom",
		JAVASCRIPTS = EXT + "javascripts",
		STYLESHEETS = EXT + "stylesheets",
		DROPEE_TOP = EXT + "dropee-top",
		MANAGABLE = EXT + "managable",
		DROPZONE = EXT + "dropzone",
		CHANGED = EXT + "changed",
		SAVING = EXT + "saving",
		DRAGEE = EXT + "dragee",
		FALSE = EXT + "false",
		TRUE = EXT + "true",
		THIS = EXT + "this",
		FILE = EXT + "file",
		BIN = EXT + "bin",
		FILE_IMG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAAAA3NCSVQICAjb4U/gAAAACXBIWXMAAARTAAAEUwECr+6lAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcG" +
			"Uub3Jnm+48GgAAAkZQTFRFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwcHAAAAGBgYAAAAAAAAAAAAAAAACQkJCQkJDQ0NDAwMDAwMHh4eHR0dHR0dHBwcIC" +
			"AgIiIiJCQkJiYmIyMjIyMjLCwsLy8vIiIiIiIiJCQkJSUlJycnKCgoKioqLCwsJCQkJSUlJycnKCgoKioqKysrLS0tLi4uKioqLS0tKCgoKysrKCgoKSkpKysrMDAwMDAwOjo6PDw8Pz8/PT09QEBAQUFBQ0NDHx" +
			"8fICAgIyMjJCQkJiYmJycnKSkpKioqKysrLCwsLS0tLi4uLy8vMDAwMTExMjIyMzMzNDQ0NTU1NjY2Nzc3ODg4OTk5Ojo6Ozs7PDw8PT09Pj4+Pz8/QEBAQUFBQkJCQ0NDRERERUVFRkZGR0dHSEhISUlJSkpKS0" +
			"tLTExMTU1NTk5OT09PUFBQUVFRUlJSU1NTVFRUVVVVVlZWV1dXWFhYWVlZWlpaW1tbXFxcXV1dXl5eX19fYGBgYWFhYmJiY2NjZGRkZWVlZmZmZ2dnaGhoampqa2trbGxsbW1tbm5ub29vcHBwcnJyc3NzdHR0dX" +
			"V1eHh4eXl5enp6e3t7gICAgYGBgoKChISEhYWFh4eHiIiIkZGRk5OTlpaWmZmZmpqanJycnZ2dnp6en5+foKCgpKSkq6ursLCwtLS0tra2t7e3uLi4u7u7vLy8vb29vr6+w8PDxsbGyMjIy8vLzs7O0tLS1dXVB+" +
			"nTgQAAAEp0Uk5TAAECAwQGBwkKCw0REhUWFxgfJikqLi8wMjY3PD5AVFdZWmlqhZWgoqKio6SkpKSkpKSlpaWlpaWlpbu7xcXGxsbT4Ovx8vP09fd9otd6AAACsElEQVQ4y21Ve0/TUBT/nduuXTfKHOAggko0xp" +
			"CgURL+8wv4af0QJibGF0LiI0QQiYrbGFvLHr299x7/2PqmyWmT29Pfq6e9BAAgIkLhYGYuLNjzc6shCstkwrHkSqNw2/tAYd30jvuz/IoFAOT5m47t05I2KXVteSx1hRpNtOOWozBKQfiufXgZZZiJNHLudd2TAI" +
			"krEsHzJysulajddif4p7oA0bwAI9c2dSBNiZrQ0U7oiJWQhmuWks54Ngz9PT68SrwvEFc6FlyLApIzo651HEXRVNnOHTWMTQERZLfquh93yWACBkws5chqPOSDMOYC4mxzJmNub4d6odKASI2X/MEcM0V0f04Ajv" +
			"oAAUwED2AQ1lcjzXkzft27BIGTEgIMgCEs4nyOUy38OoGI0ozmZVtFM47b4l5kGTCBCYUq5Di6M/W9HdPfvOhyTgGISvG4ZxPgDzBgUAUur3E1okRhUWXxFQIBI286gyxppImAmd+zoPOPlBFv3+bRqq71bVFjeL" +
			"3zssrUDF3/EheGDMAAmQyy3HgFajaUcDQUKOJRqrI8jxpkGSWacinyZ3FdmljfnCMYAXinV6dGoG7Zde5OTT7HtHFOFXsNfb1s4/xxb1369b9XHJVdE3jLkSdg8AXs+J0w9NtWFWqiJQ9XRi8eUQQDJnVTjmzUzC" +
			"QKqsOTaZxMFq+OUayKGeytDN8/Wxse75vw6Gm0Jr/tMj4NEkiRWCHx6J37YPugueF9vbu9sR7qvc/rhyEq1ODTF1CDweut5nM1ADass0vuZ9TpPKLRPvEuO7svl0dvakQ4ffXx5nmk2Y/Owen4/pe+DI5wPBkS03" +
			"fK5pEAQCzv7M8zwuIbzdXb08BkiMNUaSXHKeeolfwgrPyvPr0aI1VGjZpXs0rbQtKq42mcNVa3j8o+8h9pp2sDbmYYIwAAAABJRU5ErkJggg==",
		rsrcfll = [],
		drgee;
		
	var cE = function( e ) {
			return DOM_d.createElement( e );
		},
		jsOrCss = function( s ) {
			return ( /^User\:(.+)\.(js|css)$/ ).exec( s );
		},
		eByWN = function( w, p, n, i, nl ) {
			nl = ( w === "tag" ? p.getElementsByTagName( n ) : p.getElementsByClassName( n ) ); return i !== undefined ? nl[ i ] : nl;
		};
		
	var mngr = {
			optnnm: { local: EXT + mw.config.get( "wgUserName" ).replace( / /g, "-" ), global: "userjs-" + BASE },
			optnvlu: { js: { on: [], off: [] }, css: { on: [], off: [] } },
			css: "User:Fred_Gandt/userResourceManager.css",
			ui: cE( "li" )
		},
		
		DROP_BOT = eByWN( "class", mngr.ui, DROPEE_BOTTOM ),
		DROP_TOP = eByWN( "class", mngr.ui, DROPEE_TOP ),
		DRAG_IMG = cE( "img" ),
		
		DOM_h = eByWN( "tag", DOM_d, "head", 0 ),
		
		WG_pagename = jsOrCss( mw.config.get( "wgPageName" ) ),
		
		sssnstrg = JSON.parse( sessionStorage[ mngr.optnnm.local ] || "{}" ),
		strngyvlu = JSON.stringify( mngr.optnvlu );
		
	var notIn = function( h, n ) {
			return !~h.indexOf( n );
		},
		isJS = function( jrc ) {
			return jrc[ 2 ] === "js";
		},
		nl2a = function( nl ) {
			return [].slice.call( nl );
		},
		changed = function() {
			mngr.ui.classList.add( CHANGED );
		},
		eById = function( id ) {
			return DOM_d.getElementById( id );
		},
		highlightThis = function( ths ) {
			ths.setAttribute( "class", THIS );
		},
		getSection = function( jrc, ooo ) {
			return eByWN( "class", eById( jrc ), ooo, 0 );
		},
		present = function( s ) {
			return underSpace( s, true ).replace( "/", " - " );
		},
		resourcesOn = function() {
			return mngr.optnvlu.js.on.concat( mngr.optnvlu.css.on );
		},
		underSpace = function( s, b ) {
			return b ? s.replace( /_/g, " " ) : s.replace( / /g, "_" );
		},
		storeSession = function() {
			sessionStorage[ mngr.optnnm.local ] = JSON.stringify( { vlu: mngr.optnvlu, incld: sssnstrg.incld } );
		},
		resource = function( d, u ) {
			return present( d ) + '<a href="/wiki/' + u + '" target="_blank" title="Visit this resource page" draggable="false"></a>';
		},
		notUnmanagable = function( jrc ) {
			return jrc[ 1 ] !== "Fred_Gandt/userResourceManager" &&
				!( /^.+\/(common|cologneblue|modern|monobook|vector)$/ ).test( jrc[ 1 ] ); // Is this related to a managed resource?
		},
		clean = function() {
			mngr.ui.removeAttribute( "class" );
			eById( BIN ).innerHTML = "";
		},
		setCSS = function( txt, nre ) {
			nre = cE( "style" );
			nre.textContent = txt;
			DOM_h.appendChild( nre );
		},
		peaSoup = function( a ) {
			var v, r, o = [];
			for ( v in a ) {
				r = a[ v ];
				o.push( '<p id="' + r + '" draggable="true">' + resource( jsOrCss( r )[ 1 ], r ) + '</p>' );
			}
			return o.join( "" );
		},
		clearDropeeClasses = function( trg, ps, p ) {
			ps = nl2a( DROP_TOP ).concat( nl2a( DROP_BOT ) );
			for ( p in ps ) {
				if ( trg != ps[ p ] ) {
					ps[ p ].removeAttribute( "class" );
				}
			}
		},
		idArray = function( p ) {
			var e, o = [], a = nl2a( eByWN( "tag", p, "p" ) );
			for ( e in a ) {
				o.push( a[ e ].id );
			}
			return o;
		},
		manageThis = function( dst, jrc, ooo, p ) {
			p = cE( "p" );
			p.setAttribute( "id", jrc[ 0 ] );
			p.setAttribute( "draggable", "true" );
			p.innerHTML = resource( jrc[ 1 ], jrc[ 0 ] );
			eByWN( "class", dst, EXT + !!ooo, 0 ).appendChild( p );
			return p;
		},
		apiQuery = function( dt, fnc, mthd ) {
			dt.format = "json";
			$.ajax( {
				type: mthd || "GET",
				url: "/w/api.php",
				dataType: dt.format,
				data: dt,
				success: function( data ) { fnc( data ); },
				error: function( data) { console.error( data ); } // TODO: Inform the user
			} );
		},
		fetchResources = function( ra, tmp, tbi ) {
			// TODO Version checking with confirmation before "upgrade"?
			apiQuery( { action: "query", prop: "revisions", rvprop: "content", titles: ra.join( "|" ) }, function( data ) {
				var pgs = data.query.pages, pg, cpg, rsrc, rsrcs = {};
				for ( pg in pgs ) {
					cpg = pgs[ pg ];
					rsrcs[ underSpace( cpg.title ) ] = cpg.revisions[ 0 ][ "*" ]; //.replace( /[\t\r\n]+/g, "" ); // TODO: Develop effective minification
				}
				if ( !tmp ) {
					for ( rsrc in rsrcs ) {
						sssnstrg.incld[ underSpace( rsrc ) ] = rsrcs[ rsrc ];
					}
					storeSession();
					if ( !tbi ) {
						applyResources();
						return;
					}
				}
				applyResources( rsrcs );
			} );
		},
		applyResources = function( rsrcs ) {
			var execute = function( r, c ) {
				if ( notIn( rsrcfll, r ) ) {
					rsrcfll.push( r );
					if ( isJS( jsOrCss( r ) ) ) {
						try {
							$.globalEval( c );
						} catch ( err ) {
							console.error( r + "\n" + err ); // TODO: Inform the user
						}
					} else {
						setCSS( c ); // TODO: Prioritize setting CSS
					}
				}
			}, rsrc, crsrc;
			setCSS( sssnstrg.incld[ mngr.css ] );
			if ( rsrcs ) {
				for ( rsrc in rsrcs ) {
					if ( rsrc !== mngr.css ) {
						execute( rsrc, rsrcs[ rsrc ] );
					}
				}
			} else {
				rsrcs = resourcesOn();
				for ( rsrc in rsrcs ) {
					crsrc = rsrcs[ rsrc ];
					execute( crsrc, sssnstrg.incld[ crsrc ] );
				}
			}
		},
		dropZone = function( dz ) {
			dz.addEventListener( "dragover", function( evt, trg ) {
				evt.preventDefault();
				trg = evt.target;
				evt.dataTransfer.dropEffect = "move";
				drgee.setAttribute( "class", DRAGEE );
				if ( trg.nodeName.toLowerCase() === "p" && trg !== drgee ) {
					if ( evt.offsetY < trg.offsetHeight / 2 ) {
						trg.setAttribute( "class", DROPEE_TOP );
					} else {
						trg.setAttribute( "class", DROPEE_BOTTOM );
					}
					clearDropeeClasses( trg );
				}
			}, false );
			dz.addEventListener( "drop", function( evt, trg, trgp ) {
				evt.preventDefault();
				trg = evt.target;
				trgp = trg.parentElement;
				if ( trgp.classList.contains( DROPZONE ) ) {
					if ( evt.offsetY < trg.offsetHeight / 2 ) {
						trgp.insertBefore( drgee, trg );
					} else {
						trgp.insertBefore( drgee, trg.nextElementSibling );
					}
					changed();
				} else if ( trg.classList.contains( DROPZONE ) ) {
					trg.appendChild( drgee );
					changed();
				}
			}, false );
		},
		save = function() {
			var js_on = idArray( getSection( JAVASCRIPTS, TRUE ) ),
				css_on = idArray( getSection( STYLESHEETS, TRUE ) ),
				bnnd = idArray( eById( BIN ) ),
				on = js_on.concat( css_on ),
				tbi = [], n, rsrc, incldd, tkn;
			mngr.optnvlu = { js: { on: js_on, off: idArray( getSection( JAVASCRIPTS, FALSE ) ) }, css: { on: css_on, off: idArray( getSection( STYLESHEETS, FALSE ) ) } };
			for ( n in on ) {
				rsrc = on[ n ];
				if ( !sssnstrg.incld[ rsrc ] ) {
					tbi.push( rsrc );
				}
			}
			for ( incldd in sssnstrg.incld ) {
				if ( incldd !== mngr.css && ( notIn( on, incldd ) || !notIn( bnnd, incldd ) ) ) {
					delete sssnstrg.incld[ incldd ];
				}
			}
			if ( tbi.length ) {
				fetchResources( tbi, false, true );
			} else {
				storeSession();
			}
			apiQuery( { action: "options", token: mw.user.tokens.values.csrfToken, optionname: mngr.optnnm.global, optionvalue: JSON.stringify( mngr.optnvlu ) }, function( data ) {
				if ( data.options && data.options === "success" ) {
					clean();
				}
			}, "POST" );
		},
		setListeners = function() {
			var dzs = nl2a( eByWN( "class", mngr.ui, DROPZONE ) ), dz, jrc, ths;
			for ( dz in dzs ) {
				dropZone( dzs[ dz ] );
			}
			mngr.ui.addEventListener( "click", function( evt ) {
				var trg = evt.target, id = trg.id;
				if ( trg.nodeName.toLowerCase() === "button" ) {
					if ( id === MANAGABLE ) {
						jrc = WG_pagename;
						ths = eById( jrc[ 0 ] );
						if ( !ths ) {
							changed();
						}
						if ( isJS( jrc ) ) {
							ths = ths || manageThis( eById( JAVASCRIPTS ), jrc );
						} else {
							ths = ths || manageThis( eById( STYLESHEETS ), jrc );
							mngr.ui.classList.add( STYLESHEETS );
						}
						highlightThis( ths );
						mngr.ui.classList.add( BASE );
					} else if ( trg.classList.contains( EXT + "purge" ) && confirm( "This action will clear the session cache of resources.\n" +
							"This will NOT affect your resource configuration;\nIt will ONLY initialize refreshing the cache.\nDo you wish to continue?" ) ) {
						delete sessionStorage[ mngr.optnnm.local ];
					} else {
						mngr.ui.classList.toggle( trg.getAttribute( "class" ).replace( / |webfonts-changed/gi, "" ) );
						if ( id === SAVING ) {
							save();
						}
					}
				} else if ( id === BIN ) {
					mngr.ui.classList.toggle( BIN );
				} else if ( trg.parentElement.classList.contains( FALSE ) ) {
					if ( notIn( rsrcfll, id ) && confirm( 'Include ' + present( jsOrCss( id )[ 1 ] ).replace( " - ", "'s \"" ) + '" temporarily?' ) ) {
						fetchResources( [ id ], true );
					}
				}
			}, false );
			mngr.ui.addEventListener( "change", function( evt ) {
				var trg = evt.target, trgp = trg.parentElement;
				if ( trg.getAttribute( "type" ) === "text" ) {
					jrc = jsOrCss( trg.value.trim() ); // TODO: Accept variations of text - with or without "User:" and/or underscores etc.
					// TODO Interwiki resources?
					if ( !!jrc && notUnmanagable( jrc ) && ( ( trgp.id === JAVASCRIPTS && isJS( jrc ) ) || ( trgp.id === STYLESHEETS && !isJs( jrc ) ) ) ) {
						ths = eById( jrc[ 0 ] );
						if ( !ths ) {
							ths = manageThis( trgp, jrc );
							changed();
						}
						highlightThis( ths );
						trg.value = "";
					}
				}
			}, false );
			mngr.ui.addEventListener( "dragstart", function( evt ) {
				evt.dataTransfer.effectAllowed = "move";
				evt.dataTransfer.setDragImage( DRAG_IMG, 24, 24 );
				drgee = evt.target;
			}, false );
			mngr.ui.addEventListener( "dragend", function( evt ) {
				evt.target.removeAttribute( "class" );
				clearDropeeClasses();
				if ( !eById( BIN ).childNodes.length ) {
					mngr.ui.classList.remove( BIN );
				}
			}, false );
		},
		createUI = function( mngbl, mngd, rsrc ) {
			$( DOM_d ).ready( function() {
				DRAG_IMG.setAttribute( "class", FILE );
				DRAG_IMG.setAttribute( "src", FILE_IMG );
				mngr.ui.setAttribute( "id", BASE );
				mngr.ui.innerHTML = '<button class="' + BASE + '">User Resources</button><button id="' + MANAGABLE + '">Manage this</button><div class="' +
					BASE + '"><button class="' + STYLESHEETS + '">JavaScripts / StyleSheets</button><div id="' + JAVASCRIPTS +
					'"><input type="text" placeholder="Add a new script"><div class="' + DROPZONE + ' ' + TRUE + '">' + peaSoup( mngr.optnvlu.js.on ) +
					'</div><div class="' + DROPZONE + ' ' + FALSE + '">' + peaSoup( mngr.optnvlu.js.off ) + '</div></div><div id="' + STYLESHEETS +
					'"><input type="text" placeholder="Add a new stylesheet"><div class="' + DROPZONE + ' ' + TRUE + '">' + peaSoup( mngr.optnvlu.css.on ) +
					'</div><div class="' + DROPZONE + ' ' + FALSE + '">' + peaSoup( mngr.optnvlu.css.off ) + '</div></div><div class="' + EXT +
					'actions"><button class="' + BASE + '">Close</button><button class="' + EXT + 'purge" title="Purge session cache">Purge</button><button id="' +
					SAVING + '" class="' + SAVING + '">Save</button><div id="' + BIN + '" class="' + DROPZONE + '"></div></div><i>Saving...</i></div>';
				eByWN( "tag", eById( "p-personal" ), "ul", 0 ).appendChild( mngr.ui );
				setListeners();
				if ( !!WG_pagename && notUnmanagable( WG_pagename ) ) {
					mngr.ui.classList.add( MANAGABLE );
					if ( mngbl = eById( WG_pagename[ 0 ] ) ) {
						highlightThis( mngbl );
					}
				}
			} );
		},
		init = function() {
			sssnstrg.incld = {};
			fetchResources( resourcesOn().concat( [ mngr.css ] ) );
			createUI();
		};
	if ( sssnstrg.incld ) {
		mngr.optnvlu = sssnstrg.vlu;
		applyResources();
		createUI();
	} else {
		mngr.optnvlu = JSON.parse( mw.user.options.values[ mngr.optnnm.global ] || strngyvlu );
		init();
	}
} ( document ) );