User:EpochFail/mc system.js

// MC = { escape: function(custom){ return "" + custom.replace("<", "&lt;").replace(">", "&gt;") + "" } }

/******************************************************************************* Utility functions useful for working in Javascript



if(typeof(String.prototype.trim) === "undefined"){ String.prototype.trim = function{ return String(this).replace(/^\s+|\s+$/g, ''); }; }

/* Simple JavaScript Inheritance * By John Resig http://ejohn.org/ * MIT Licensed. */ // Inspired by base2 and Prototype (function{ var initializing = false, fnTest = /xyz/.test(function{xyz;}) ? /\b_super\b/ : /.*/;  // The base Class implementation (does nothing)  this.Class = function{};  // Create a new Class that inherits from this class  Class.extend = function(prop) {    var _super = this.prototype;    // Instantiate a base class (but only create the instance, // don't run the init constructor)   initializing = true;    var prototype = new this;    initializing = false;    // Copy the properties over onto the new prototype    for (var name in prop) {      // Check if we're overwriting an existing function      prototype[name] = typeof prop[name] == "function" &&         typeof _super[name] == "function" && fnTest.test(prop[name]) ?        (function(name, fn){ return function { var tmp = this._super; // Add a new ._super method that is the same method // but on the super-class this._super = _super[name]; // The method only need to be bound temporarily, so we           // remove it when we're done executing var ret = fn.apply(this, arguments); this._super = tmp; return ret; };       })(name, prop[name]) :        prop[name];    }    // The dummy class constructor    function Class {      // All construction is actually done in the init method      if ( !initializing && this.init )        this.init.apply(this, arguments);    }    // Populate our constructed prototype object    Class.prototype = prototype;    // Enforce the constructor to be what we expect    Class.prototype.constructor = Class;

// And make this class extendable Class.extend = arguments.callee; return Class; }; });

/** * This class represents the top level Mr. Clean interface. It primarily exists as * a broker between all part of the system that need to communicate. This class * also handles the highest-level events.. */ MC.Interface = Class.extend({	init: function(article, categories){		this.article = article		this.picker = new MC.TemplatePicker( categories, function(interface){return function{ //insert interface.insertTemplate }}(this), function(interface){return function{ //cancel interface.closePicker }}(this) )	},	/**	 * Loads the visual interface by manipulating the DOM to insert togglers.	 */	load: function{		var expand = function(interface){return function(toggler){			if(interface.toggler){interface.toggler.contract}			toggler.expand(interface.picker)			interface.toggler = toggler		}}(this)		var contract = function(interface){return function(toggler){			toggler.contract			interface.picker.contract		}}(this)		$('span.mw-editsection').each(function(expand, contract){return function(i, element){ var element = $(element) var toggler = MC.CleanupToggler.fromElement(				element,				expand,				contract			) element.prepend(toggler.span) }}(expand, contract))		var toggler = new MC.CleanupToggler(0, $('h1.firstHeading'), expand, contract)		$('h1.firstHeading').append($(" ").addClass("editsection").append(toggler.span))	},	/**	 * Performs the insert operation using the server and manipulates the 	 * interface in response.	 */	insertTemplate: function{		this.picker.disable//disable the interface 		MC.server.prependOnSection( this.article.title, this.picker.section, this.picker.form.construct, function(interface){return function(html){ //success interface.picker.div.after($(interface.picker.form.pane.div.html)) //copy the last preview to the document interface.closePicker }}(this), function(interface){return function(message){ //error alert(message) //tell the user what happened interface.picker.enable //re-enable so the user can try again }}(this) )	},	/**	 * Closes the template picker and resets the interface.	 */	closePicker: function{		this.picker.contract		this.toggler.contract		this.toggler = null	} })

/** * This class represents a template picking visual interface. */ MC.TemplatePicker = Class.extend({	/**	 * Constructor	 *	 * @param templateCategories A list of categories containing a list of template metadata to load into the picker.	 * @param insert            A function to be called when the template insert function is called	 * @param cancel             A function to be called when the template cancel function is called	 */	init: function(templateCategories, insert, cancel){		this.section = null		this.header = null		this.div = $(" ")			.addClass("template_picker")		var table = $(" ")		this.div.append(table)		var row = $(" ")		table.append(row)		this.container = $(" ")			.addClass("picker_container")		row.append(this.container)		this.categories = {			div: $(" ")				.addClass("categories")		}		this.container.append(this.categories.div)		this.templates = {			div: $(" ")				.addClass("templates")		}		this.container.append(this.templates.div)		this.form = new MC.TemplateForm(insert, cancel) this.container.append(this.form.div) for(var i in templateCategories){ var cat = templateCategories[i] var category = this.addCategory(cat.name, cat.icon, cat.templates) if(!this.category){ this.category = category }		}	},	/**	 * Disables all interations with the picker. */	disable: function{ this.div.addClass("disabled") this.form.disable },	/**	 * Re-enables all interations with the picker. */	enable: function{ this.div.removeClass("disabled") this.form.enable },	/**	 * Expands the picker beneath a section header. *	 * @params section The section number * @params header The jQuery header element ( - ) */	expand: function(section, header){ this.contract(			function(picker, section, header){return function{				picker.section = section				picker.header = header				if(section === 0){					picker.div.removeClass("section")					picker.div.addClass("article")					$('#mw-content-text').prepend(picker.div)				}else{					picker.div.removeClass("article")					picker.div.addClass("section")					header.after(picker.div)				}				picker.selectCategory(picker.category)				picker.div.slideDown(100)				picker.enable			}}(this, section, header)		) },	/**	 * Hides and resets the interface. *	 * @param callback Function to be called when the interface has finished being hidden. */	contract: function(callback){ this.section = null //No section this.header = null  //No header this.form.contract //Hide the form if(this.category){this.category.clear} //Reset the category if(this.div.is(":visible")){ this.div.slideUp(100, callback) }else{ callback }	},	/**	 * Adds a new category of templates to the picker *	 * @param name The name to display for the template category * @param icon The icon file to load into the background * @param templates A list of template metadata to load for this category */	addCategory: function(name, icon, templates){ category = new MC.TemplateCategory(			name,			icon,			function(picker){return function(name){				if(!picker.div.hasClass("disabled")){					picker.selectCategory(name)				}			}}(this),			function(picker){return function(name){				if(!picker.div.hasClass("disabled")){					picker.selectTemplate(name)				}			}}(this)		) for(var i in templates){ category.templates.add(templates[i]) }		this.categories[name] = category this.categories.div.append(category.div) return category },	/**	 * Selects a category and displays its templates (depending on the context) * 	 * @param name The name of the category to select */	selectCategory: function(category){ if(this.category){ this.category.deselect this.form.contract }		if(this.section == 0){ var context = "article" }else{ var context = "section" }		this.category = category this.category.select this.templates.div.children.detach this.templates.div.append(this.category.templates.getForContext(context)) },	/**	 * Select a template and loads it into the form *	 * @param template The template to select and load. */	selectTemplate: function(template){ if(!this.category){ throw "No template category selected" }else{ if(this.section == 0){ var context = "article" }else{ var context = "section" }			this.form.expand(template, context) }	} })

/** * A form that displays a template and (optionally) accepts customization. */ MC.TemplateForm = Class.extend({	/**	 * Constructor	 *	 * @param insert A function to call when the insert event is started	 * @param cancel A function to call when the cancel event is started	 */	init: function(insert, cancel){		this.div = $(" ")			.addClass("form")			.hide		this.pane = {			div: $(" ")				.addClass("pane")		}		this.div.append(this.pane.div)		this.custom = {			label: $(" ")				.addClass("custom")				.text("customize:")				.attr("for", "customize_textbox"),			input: $(" ")				.addClass("custom")				.attr('type', "text")				.attr('id', "customize_textbox")				.keyup( function(form){return function{ //Kill last timeout if(form.lastTimeout){clearTimeout(form.lastTimeout)} //Create new timeout form.lastTimeout = setTimeout(							function(form){return function{								form.preview							}}(form),							200						) }}(this) )		}		this.div.append(this.custom.label)		this.div.append(this.custom.input)		this.buttons = {			div: $(" ")				.addClass("buttons"),			cancel: {				div: $(" ")					.addClass("cancel")					.addClass("button")					.text("cancel")					.click( function(form, cancel){return function{ if(!form.div.hasClass("disabled")){ cancel }						}}(this, cancel) )			},			insert: {				div: $(" ")					.addClass("insert")					.addClass("button")					.addClass("primary")					.text("insert")					.click( function(form, insert){return function{ if(!form.div.hasClass("disabled")){ insert }						}}(this, insert) )			}		}		this.div.append(this.buttons.div)		this.buttons.div.append(this.buttons.cancel.div)		this.buttons.div.append(this.buttons.insert.div)	},	/**	 * Disables the form to that the user may not interact with it.	 */	disable: function{		this.div.addClass("disabled")		this.custom.input.prop('disabled', true)	},	/**	 * Re-enables the form to that the user may interact with it.	 */	enable: function{		this.div.removeClass("disabled")		this.custom.input.prop('disabled', false)	},	/**	 * Shows the form and loads a template into the right context	 *	 * @param template The template to load	 * @param context The context in which to load the template	 */	expand: function(template, context){		this.template = template		this.context  = context		this.custom.input.val('')		if(template.custom){			this.custom.input.show			this.custom.label.show			this.custom.label.html(template.custom)			if(template.placeholder){ this.custom.input.val(template.placeholder) this.custom.input.focus(function(form, template){return function(e){					if(form.custom.input.val == template.placeholder){						form.custom.input.val("")					}				}}(this, template)) this.custom.input.blur(function(form, template){return function(e){					if(form.custom.input.val == ""){						form.custom.input.val(template.placeholder)						form.preview					}				}}(this, template)) }		}else{ this.custom.input.hide this.custom.label.hide this.custom.label.html(':(')		}		this.div.slideDown(100)		this.pane.div.html()		this.preview	},	/**	 * Hides the form	 */	contract: function{		this.template = null		this.pane.div.html()		this.div.slideUp(100)	},	/**	 * Updates the preview of a template (given customizations [or not])	 */	preview: function{		if(this.template){			this.pane.div.addClass("loading")			MC.server.preview( MC.article.title, this.construct, function(form){return function(html){ form.pane.div.html(html) form.pane.div.removeClass("loading") }}(this), function(form){return function(message){ alert(message) form.pane.div.removeClass("loading") }}(this) )		}	},	/**	 * Uses the displayed template and customizations to construct the 	 * Wiki-Markup necessary to render the template in the page.	 */	construct: function{		if(this.template){			return this.template.construct(this.context, this.custom.input.val)		}else{			throw "No template selected!"		}	} })

/** * Represents a selectable category of templates */ MC.TemplateCategory = Class.extend({	/**	 * Constructor	 *	 * @param name The display name of the category	 * @param icon An icon image to load into the background of the visual element	 * @param selectCategory A callback function for the selection of a category	 * @param selectTemplate A callback function for the selection of a template (within the category)	 */	init: function(name, icon, selectCategory, selectTemplate){		this.name = name		this.div = $(" ")			.addClass("category")			.text(name)			.click( function(category, select){return function{ if(!category.div.hasClass("selected")){ select(category) }				}}(this, selectCategory) )		if(icon){			this.div.css('background-image', 'url("' + icon + '")')		}		this.templates = new MC.CategoryTemplates(selectTemplate)	},	/**	 * Change the mode of presentation to "selected"	 */	select: function{		this.div.addClass("selected")	},	/**	 * Change the mode of from to "selected" to "deselected"	 */	deselect: function{		this.div.removeClass("selected")	},	/**	 * Clears selection of any templates.	 */	clear: function{		this.templates.clear	} })

/** * Represents a collection of templates and is able to display different * templates based on their supported contexts. */ MC.CategoryTemplates = Class.extend({	/**	 * Constructor	 *	 * @param selectTemplate A callback function for template selection events	 */	init: function(selectTemplate){		this.selectTemplate = selectTemplate		this.selected = null		this.div = $(" ")			.addClass("templates")		this.templates = []	},	/**	 * Adds a template to the list based on metadata	 *	 * @param t Template metadata object	 */	add: function(t){		var template = new MC.Template( t.name, t.construct, t.contexts, t.custom, t.placeholder || "", function(templates){return function(template){ templates.select(template) }}(this) )		this.templates.push(template)		this.div.append(template.div)	},	/**	 * Clears any selected templates.	 */	clear: function{		if(this.selected){			this.selected.deselect			this.selected = null		}	},	/**	 * Selects a template and deselects any previously selected templates.	 */	select: function(template){		this.clear		this.selected = template		this.selected.select		this.selectTemplate(template)	},	/**	 * Returns the a jQuery element containing the div elemets for all	 * templates that support `context`.	 *	 * @param context The context for which to gather templates	 */	getForContext: function(context){		var div = $(" ")		for(var i in this.templates){			var template = this.templates[i]			if(template.div.hasClass(context)){				div.append(template.div)			}		}		return div	} })

/** * A visual element representing a template * */ MC.Template = Class.extend({	/**	 * Constructor	 *	 * @param name The display name for a template	 * @param construct A function for constructing the wiki-markup for a template	 * @param custom A custom field label or `undefined` for no customization	 * @param select A function to be called on a select event	 */	init: function(name, construct, contexts, custom, placeholder, select){		this.name       = name		this.construct   = construct		this.custom      = custom		this.placeholder = placeholder		this.div = $(" ")			.addClass("template")			.text(name)			.click( function(template, select){return function{ select(template) }}(this, select) )		for(var i in contexts){			this.div.addClass(contexts[i])		}	},	/**	 * Sets the presentation to "selected"	 */	select: function{		this.div.addClass("selected")	},	/**	 * Sets the presentation to "deselected"	 */	deselect: function{		this.div.removeClass("selected")	} })

/* A regular expression to be used to search for the section number in a href of a section edit link */ MC.SECTION_HREF = /&section=([0-9]+)$/

/** * A toggler element used to expand and contact a picker * */ MC.CleanupToggler = Class.extend({	/**	 * Constructor	 *	 * @param section The section number to associate with	 * @param header A jQuery header element to associate with	 * @param expand A callback for the expand event	 * @param constract A callback for the contract event	 */	init: function(section, header, expand, contract){		this.header = header		this.section = section		this.span    = $(" ")			.addClass("cleanup_toggler")			.text("add note")			.click( function(toggler, expand, contract){return function(e){ if(!toggler.span.hasClass("expanded")){ expand(toggler) }else{ contract(toggler) }				}}(this, expand, contract) )		this.picker = null	},	expand: function(picker){		this.span.addClass("expanded")		picker.expand(this.section, this.header)		this.span.text("cancel")	},	contract: function{		this.span.removeClass("expanded")		this.span.text("add note")	} }) /** * Constructs a toggler from a header element */ MC.CleanupToggler.fromElement = function(element, expand, contract){ header = element.parent a = element.children("a") try { section = parseInt(a.attr('href').match(MC.SECTION_HREF)[1]); } catch(e) { section = -1; } return new MC.CleanupToggler(section, header, expand, contract) }

/** * Simplifies use of a MediaWiki API by offering a few convenience functions * and common error handling. */ MediaWikiAPI = Class.extend({	/**	 * Constructor	 * 	 * @param uri The uri to call to access the API	 */	init: function(uri, prefix){		this.uri   = uri		this.prefix = prefix || ""	},	/**	 * Sends a request to the API and handles the response	 *	 * @param data A map of request parameters to send to the API	 * @param type The type of request to make (commonly POST or GET)	 * @param success A function to call with response information upon a successful interaction	 * @param error A function to call when an error occurred	 */	request: function(data, type, success, error){		type = type || "GET"		data.format="json"		pdata = {}		for(key in data){			pdata[this.prefix + key] = data[key]		}		$.ajax({ url: this.uri, dataType: "json", data: pdata, type: type, context: this, success: function(success, error){return function(json, status){ if(status != "success"){ error("The API could not be reached: " + status) }else if(json.error){ error("Received an error from the API: " + json.error.code + " - " + json.error.info) }else{ success(json) }			}}(success, error), error: function(error){return function(jqXHR, status, message){ //Sometimes an error happens when the request is //interrupted by the user changing pages. if(status != 'error' || message != ''){ error("The API could not be reached: " + status + ": " + message) }			}}(error) })	},	/**	 * Convenience function for initiating a POST request: e.g. request(data, "POST", success, error)	 *	 * @param data A map of request parameters to send to the API	 * @param success A function to call with response information upon a successful interaction	 * @param error A function to call when an error occurred	 */	post: function(data, success, error){		return this.request(data, "POST", success, error)	},	/**	 * Convenience function for initiating a GET request: e.g. request(data, "GET", success, error)	 *	 * @param data A map of request parameters to send to the API	 * @param success A function to call with response information upon a successful interaction	 * @param error A function to call when an error occurred	 */	get: function(data, success, error){		return this.request(data, "GET", success, error)	} })

/* Used to identify the header at the beginning of a section of Wiki-markup */ MC.HEADER_RE = /^\n*=[^=].*=|==[^=].*==|===[^=].*===|====[^=].*====|=====[^=].*=====|======[^=].*======/

/** * Represents the server functions necessary for the Mr. Clean interface * * Notice that this class extends `MediaWikiAPI` */ MC.Server = MediaWikiAPI.extend({	/**	 * Gets a preview of wiki-markup in HTML	 *	 * @param title The title of the page	 * @param markup The wiki-markup to preview	 * @param success A function to call with the response	 */	preview: function(title, markup, success, error){		this.post( {				action: "parse", title: title, text: markup, pst: true, prop: "text" },			function(success, error){return function(json){ if(json.parse && json.parse.text){ success(json.parse.text['*']) }else{ error("API gave unexpected response format") }			}}(success, error), error )	},	/**	 * Prepends a chunk of markup after the header at the beginning of a 	 * or directly at the top of a page.	 *	 * @param title The title of the page to edit	 * @param section The section number to prepend to	 * @param markup The markup to prepend	 * @param success A function to call on success	 * @param error A function to call on error	 * @param edittoken The edit token to use to make the edit	 * @param content The content of the article section	 */	prependOnSection: function(title, section, markup, success, error, edittoken, content){		if(!edittoken){			this.post( {					action: "query", prop: "info|revisions", titles: title, intoken: "edit", rvprop: "content", rvsection: section },				function(server, title, section, markup, success, error){return function(json){ if(json.query && json.query.pages){ for(pageId in json.query.pages){ var page = json.query.pages[pageId] }						if(page.edittoken){ server.prependOnSection(								title, 								section, 								markup, 								success, 								error, 								page.edittoken, 								page.revisions[0]["*"]							) }else if(page.missing){ error("The page to be edited no longer exists") }					}else{ error("API gave unexpected response format") }				}}(this, title, section, markup, success, error), error )		}else{			var match = content.match(MC.HEADER_RE)			if(match){				var text = match[0] + "\n" + markup + content.substring(match[0].length)			}else{				var text = markup + content			}			this.post( {					action: "edit", title: title, section: section, text: text, token: edittoken, summary: "Inserting template " + markup + " using (MC)." },				function(success, error){return function(json){ if(json.edit && json.edit.result){ if(json.edit.result == "Success"){ success }else{ error("Could not complete edit: " + json.edit.result) }					}else{ error("API gave an unexpected response format.") }				}}(success, error), error )		}	} })

$.extend(	MC,	{		article: {			id: mw.config.get('wgArticleId'),			title: mw.config.get('wgPageName')		},		param_prefix: window.wgParamPrefix || "",		api_uri: window.location.protocol + mw.config.get('wgServer') + mw.config.get('wgScriptPath') + "/api.php",		categories: [			{				name: "style",				icon: "http://upload.wikimedia.org/wikipedia/en/thumb/f/f2/Edit-clear.svg/20px-Edit-clear.svg.png",				templates: [					{						name: "abbreviations",						custom: "Example abbreviations",						placeholder: "type abbrevations here",						contexts: ['article', 'section'],						construct: function(context, custom){							if(custom && custom.trim != ''){								custom = "|abbreviations (e.g. " + MC.escape(custom) + ")"							}else{								custom = ""							}							context = "|" + context							return ""						}					},					{						name: "buzzwords",						custom: "Example buzzwords",						placeholder: "example buzzwords here", contexts: ['article', 'section'], construct: function(context, custom){ if(custom && custom.trim != ''){ custom = "|buzzwords (e.g. " + MC.escape(custom) + ")" }else{ custom = "" }							context = "|" + context return "" }					},					{						name: "tense", custom: "Please consider copy editing to", placeholder: "describe tense change here", contexts: ['article', 'section'], construct: function(context, custom){ if(custom && custom.trim != ''){ custom = "|tense=" + MC.escape(custom) }else{ custom = "" }							context = "|" + context return "" }					},					{						name: "editorial", contexts: ['article', 'section'], construct: function(context){ context = "|" + context return "" }					},					{						name: "plot", contexts: ['article', 'section'], construct: function(context){ context = "|" + context return "" }					},					{						name: "redundant", contexts: ['article', 'section'], construct: function(context){ context = "|" + context return "" }					},					{						name: "context", contexts: ['article', 'section'], construct: function(context){ context = "|" + context return "" }					},					{						name: "overly detailed", contexts: ['article', 'section'], construct: function(context){ if(context == "section"){ context = "|section=yes" }else{ context = "|section=no" }							return "" }					},					{						name: "general cleanup", custom: "The specific problem is", contexts: ['article', 'section'], placeholder: "describe problem here", construct: function(context, custom){ context = "|" + context if(custom && custom.trim != ''){ custom = "|reason=" + MC.escape(custom) }else{ custom = "|reason=" }							return "" }					}				]			},			{				name: "content", icon: "http://upload.wikimedia.org/wikipedia/en/thumb/f/f4/Ambox_content.png/20px-Ambox_content.png", templates: [ {						name: "importance", contexts: ['section'], construct: function{ return "" }					},					{						name: "fiction", contexts: ['article', 'section'], construct: function(context){ context = "|" + context return "" }					},					{						name: "advertisement", contexts: ['article', 'section'], construct: function(context){ context = "|" + context return "" }					},					{						name: "importance", contexts: ['section'], construct: function{ return "" }					},					{						name: "promotional", contexts: ['section', 'article'], construct: function(context){ context = "|" + context return "" }					},					{						name: "trivia", contexts: ['section'], construct: function{ return "" }					},					{						name: "off-topic", contexts: ['section'], construct: function{ return "" }					},					{						name: "missing info", custom: "Missing information about", contexts: ['article', 'section'], placeholder: "", construct: function(context, custom){ if(custom && custom.trim != ''){ custom = "|" + MC.escape(custom) }else{ custom = "|" }							context = "|" + context return "" }					},				]			}		]	} ) MC.server = new MC.Server(MC.api_uri, MC.param_prefix) MC.interface = new MC.Interface(MC.article, window.MC_CATEGORIES || MC.categories) $(document).ready( function{ MC.interface.load } ) //