Jump to content

User:Evad37/rater.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Evad37 (talk | contribs) at 15:45, 10 November 2017 (fix add project button). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
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.
/***************************************************************************************************
 Rater --- by Evad37
 > Helps assess WikiProject banners.
 > Main version: 0-alpha (for full version, see config.script.version below)
***************************************************************************************************/
// <nowiki>
$( function($) {
/* ========== Config ============================================================================ */
// A global object that stores all the page and user configuration and settings
var config = {};
// Script info
config.script = {
	// Advert to append to edit summaries
	advert:  ' ([[User:Evad37/rater.js|Rater]])',
	version: '0.2.0-alpha'
};
// MediaWiki configuration values
config.mw = mw.config.get( [
	'wgPageName',
	'wgNamespaceNumber',
	'wgUserName',
	'wgFormattedNamespaces',
	'wgMonthNames',
	'wgRevisionId'
] );
config.regex = {
	// Pattern to find templates, which may contain other templates
	template:		/\{\{\s*(.+?)\s*(\|(?:.|\n)*?(?:(?:\{\{(?:.|\n)*?(?:(?:\{\{(?:.|\n)*?\}\})(?:.|\n)*?)*?\}\})(?:.|\n)*?)*|)\}\}\n?/g,
	// Pattern to find `|param=value` or `|value`, where `value` can only contain a pipe
	// if within square brackets (i.e. wikilinks) or braces (i.e. templates)
	templateParams:	/\|(?!(?:[^{]+}|[^\[]+]))(?:.|\s)*?(?=(?:\||$)(?!(?:[^{]+}|[^\[]+])))/g
};
config.deferred = {};
config.bannerDefaults = {
	classes: [
		' ',
		'FA',
		'FL',
		'A',
		'GA',
		'B',
		'C',
		'Start',
		'Stub',
		'List'
	],
	importances: [
		' ',
		'Top',
		'High',
		'Mid',
		'Low'
	],
	extendedClasses: [
		'Category',
		'Draft',
		'File',
		'Portal',
		'Project',
		'Template',
		'Bplus',
		'Future',
		'Current',
		'Disambig',
		'NA',
		'Redirect',
		'Book'
	],
	extendedImportances: [
		'Bottom',
		'NA'
	]
};
config.shellTemplates = [
	'WikiProject banner shell',
	'WikiProjectBanners',
	'WikiProject Banners',
	'WPB',
	'WPBS',
	'Wikiprojectbannershell',
	'WikiProject Banner Shell',
	'Wpb',
	'WPBannerShell',
	'Wpbs',
	'Wikiprojectbanners',
	'WP Banner Shell',
	'WP banner shell',
	'Bannershell',
	'Wikiproject banner shell',
	'WikiProject Banners Shell',
	'WikiProjectBanner Shell',
	'WikiProjectBannerShell',
	'WikiProject BannerShell',
	'WikiprojectBannerShell',
	'WikiProject banner shell/redirect',
	'WikiProject Shell',
	'Banner shell',
	'Scope shell',
	'Project shell'
];

// Do not operate on Special: pages, nor on non-existent pages or their talk pages
if ( config.mw.wgNamespaceNumber < 0 || $('li.new[id|=ca-nstab]').length ) {
	return;
}
// For User and User_talk namespaces, only operate on subpages
if (
	config.mw.wgNamespaceNumber >= 2 &&
	config.mw.wgNamespaceNumber <= 3 &&
	config.mw.wgPageName.indexOf('/') === -1
) {
	return;
}

// Load Morebits gadget if not already available
if ( window.Morebits == null ) {
	importScript('MediaWiki:Gadget-morebits.js');
	importStylesheet( 'MediaWiki:Gadget-morebits.css' );
}
// Load extra.js if not already available
if ( window.extraJs == null ) {
	importScript('User:Evad37/extra.js');
}

mw.loader.using( ['mediawiki.util'], function () {
// Add portlet link
mw.util.addPortletLink( 'p-cactions', '#', 'Rater', 'ca-rater', 'Rate quality and importance' );
$('#ca-rater').click(function(e) {

e.preventDefault();
mw.loader.using( ['mediawiki.api', 'mediawiki.Title', 'mediawiki.RegExp',
	'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows', 'jquery.ui.dialog'], function () {

/* ========== CSS =============================================================================== */
// TODO: convert to .css subpage and load using importStylesheet()
// Attribution: Diff styles from <https://en.wikipedia.org/wiki/Wikipedia:AutoWikiBrowser/style.css>
mw.util.addCSS(
	/* --- Main dialog styles ----------------------------------------------------------------- */
	'.rater-dialog-row { padding:0.2em; border-bottom: 1px solid #777; }'+
	'.rater-dialog-row span { padding-right:0.5em; white-space:nowrap }'+
	'.rater-dialog-row:nth-child(even) { background-color:#e0e0e0; }'+
	'.rater-dialog-row > div:nth-child(2) { clear:left; }'+
	'.rater-dialog-paraInput input { width:6em; margin:0 0.15em; }'+
	'.rater-dialog-autofill { border:1px dashed #cd20ff; padding:0.2em; margin-right:0.2em; }'+
	'.rater-dialog-autofill::after { content:"autofilled"; color:#cd20ff; font-weight:bold; font-size:96% }'+
	'.rater-dialog-dropdown { width:5em; margin-left:0.2em; }'+
	/* --- Diff styles -----------------------------------------------------------------------  */
	'table.diff, td.diff-otitle, td.diff-ntitle { background-color: white; }'+
	'td.diff-otitle, td.diff-ntitle { text-align: center; }'+
	'td.diff-marker { text-align: right; font-weight: bold; font-size: 1.25em; }'+
	'td.diff-lineno { font-weight: bold; }'+
	'td.diff-addedline, td.diff-deletedline, td.diff-context { font-size: 88%; vertical-align: top; white-space: -moz-pre-wrap; white-space: pre-wrap; }'+
	'td.diff-addedline, td.diff-deletedline { border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; }'+
	'td.diff-addedline { border-color: #a3d3ff; }'+
	'td.diff-deletedline { border-color: #ffe49c; }'+
	'td.diff-context { background: #f3f3f3; color: #333333; border-style: solid; border-width: 1px 1px 1px 4px; border-color: #e6e6e6; border-radius: 0.33em; }'+
	'.diffchange { font-weight: bold; text-decoration: none; }'+
	'table.diff { border: none; width: 98%; border-spacing: 4px;'+
		/* Ensure that colums are of equal width: */ 'table-layout: fixed; }'+
	'td.diff-addedline .diffchange, td.diff-deletedline .diffchange { border-radius: 0.33em; padding: 0.25em 0; }'+
	'td.diff-addedline .diffchange {	background: #d8ecff; }'+
	'td.diff-deletedline .diffchange { background: #feeec8; }'+
	'table.diff td {	padding: 0.33em 0.66em; }'+
	'table.diff col.diff-marker { width: 2%; }'+
	'table.diff col.diff-content { width: 48%; }'+
	'table.diff td div {'+
		/* Force-wrap very long lines such as URLs or page-widening char strings. */
		'word-wrap: break-word;'+
		/* As fallback (FF<3.5, Opera <10.5), scrollbars will be added for very wide cells
		   instead of text overflowing or widening */
		'overflow: auto;'+
	'}'
);
	
/* ========== API =============================================================================== */
var API = new mw.Api( {
    ajax: {
        headers: { 
			'Api-User-Agent': 'Rater/' + config.script.version + 
				' ( https://en.wikipedia.org/wiki/User:Evad37/Rater )'
		}
    }
} );
/* ---------- API for ORES ---------------------------------------------------------------------- */
API.getORES = function(revisionID) {
	return $.get('https://ores.wikimedia.org/v3/scores/enwiki?models=wp10&revids='+revisionID);
};

/* ========== Page class ======================================================================== */
// Constructor
var Page = function(title) {
	try {
		mw.Title.call(this, decodeURIComponent(title));
	} catch(e) {
		throw new Error('Unable to parse title "'+title+'"'); 
	}
	this.talk = null;
	this.subject = null;
	this.banners = [];
};
//Constructor with a null return instead of an exception for invalid titles.
Page.newFromText = function(t) {
	if ( mw.Title.newFromText(t) ) {
		return new Page(t);
	} else {
		return null;
	}
};

// ---------- Page prototype -------------------------------------------------------------------- */
// Inherited from mw.Title
Page.prototype = Object.create(mw.Title.prototype);
Page.prototype.constructor = Page;
// Additional functions
Page.prototype.getTalk = function() {
	if ( this.talk === null ) {
		// talk page not yet set, so set it now
		if ( this.getNamespaceId()%2 === 1 ) {
			// Page is itself a talk page
			this.talk = this.getPrefixedText();
		} else {
			this.talk = mw.Title.newFromText(
				this.getMain(),
				this.getNamespaceId()+1
			).getPrefixedText();
		}
	}
	return this.talk;
};
Page.prototype.getSubject = function() {
	if ( this.subject === null ) {
		// subject page not yet set, so set it now
		if ( this.getNamespaceId()%2 === 0 ) {
			// Page is itself a subject page
			this.subject = this.getPrefixedText();
		} else {
			this.subject = mw.Title.newFromText(
				this.getMain(),
				this.getNamespaceId()-1
			).getPrefixedText();
		}
	}
	return this.subject;
};
Page.prototype.getListasAutofill = function() {
	var name = this.getMainText().replace(/\s\(.*\)/, '');
	var lastSpaceIndex = name.lastIndexOf(' ');
	return name.slice(lastSpaceIndex+1) + ', ' + name.slice(0, lastSpaceIndex);
};
	
Page.prototype.getRedirectOrPrefixedText = function() {
	return ( this.redirectsTo ) ? this.redirectsTo.getPrefixedText() : this.getPrefixedText();
};
Page.prototype.getRedirectOrMainText = function() {
	return ( this.redirectsTo ) ? this.redirectsTo.getMainText() : this.getMainText();
};
Page.prototype.getBannerFromNameOrRedirect = function(bannerNameOrRedirect) {
	if ( !this.banners ) {
		return false;
	}
	
	var toCheckPrefixedText = mw.Title.newFromText('Template:'+bannerNameOrRedirect).getPrefixedText();
	
	for ( var i=0; i<this.banners.length; i++ ) {
		if (
			this.banners[i].getPrefixedText() === toCheckPrefixedText ||
			this.banners[i].getRedirectOrPrefixedText() === toCheckPrefixedText
		) {
			return this.banners[i];
		}
	}
	return false;
};
Page.prototype.getTalkpageTopSection = function() {
	var self = this;
	var gotTalkpageTopSection = $.Deferred();
	
	var processTalk = function (result) {
		var id = result.query.pageids;		
		self.oldTopSection = ( id < 0 ) ? '' : result.query.pages[id].revisions[0]['*'];
		gotTalkpageTopSection.resolve();
	};
	
	API.get( {
		action: 'query',
		prop: 'revisions',
		rvprop: 'content',
		rvsection: '0',
		titles: self.getTalk(),
		indexpageids: 1
	} )
	.done( processTalk )
	.fail( gotTalkpageTopSection.reject );	
	
	return gotTalkpageTopSection;
};
Page.prototype.getLatestSubjectRevisionID = function() {
	
	var self = this;
	var gotRevisionID = $.Deferred();
	
	if  ( config.mw.wgNamespaceNumber === 0 && config.mw.wgRevisionId !== 0 ) {
		self.latestSubjectRevisionID = config.mw.wgRevisionId;
		return gotRevisionID.resolve();
	}
	
	var processRevision = function(result) {		
		var id = result.query.pageids;
		if ( id < 0 ) {
			gotRevisionID.reject();
			return;
		}
		self.latestSubjectRevisionID = result.query.pages[id].revisions[0].revid;
		gotRevisionID.resolve();
	};
	
	API.get( {
		action: 'query',
		format: 'json',
		prop: 'revisions',
		titles: self.getSubject(),
		rvprop: 'ids',
		indexpageids: 1
	} )
	.done( processRevision )
	.fail( gotRevisionID.reject );	
	
	return gotRevisionID;	
};

Page.prototype.getOresScore = function() {
	var self = this;
	var gotOresScore = $.Deferred();
	
	$.when( self.latestSubjectRevisionID || self.getLatestSubjectRevisionID() )
	.done( function() {
		API.getORES(self.latestSubjectRevisionID)
		.done(function(result) {
			var data = result.enwiki.scores[self.latestSubjectRevisionID].wp10;
			if ( data.error ) {
				gotOresScore.reject(data.error.type, data.error.message);
				return;
			}
			self.oresScore = data.score.prediction;
			gotOresScore.resolve();
		})
		.fail( gotOresScore.reject );
	})
	.fail( gotOresScore.reject );
	
	return gotOresScore;
	
};

Page.prototype.makeEdit = function() {
	var self = this;
	var editMade = $.Deferred();
	
	API.postWithToken( 'csrf', {
		action: 'edit',
		title: self.getTalk(),
		text: self.makeNewTopSection(),
		section: 0,
		summary: self.makeEditSummary() + config.script.advert
	} )
	.done( editMade.resolve )
	.fail( editMade.reject );
	return editMade;
};
Page.prototype.makePreview = function() {
	var self = this;
	var madePreview = $.Deferred();
	
	API.get({
		action: 'parse',
		contentmodel: 'wikitext',
		text: self.makeNewTopSection(),
		title: self.getTalk()
	})
	.done(function(result) {
		if ( !result || !result.parse || !result.parse.text || !result.parse.text['*'] ){
			madePreview.reject('Empty result');
		}
		madePreview.resolve(result.parse.text['*']);
	})
	.fail(madePreview.reject);
	
	return madePreview;
};
Page.prototype.makeDiff = function() {
	var self = this;
	var madeDiff = $.Deferred();
	
	API.get({
		action: "compare",
		format: "json",
		fromtext: self.oldTopSection,
		fromcontentmodel: "wikitext",
		totext: self.makeNewTopSection(),
		tocontentmodel: "wikitext",
		prop: "diff"
	})
	.done(function(result) {
		if ( !result || !result.compare || !result.compare['*'] ){
			madeDiff.reject('Empty result');
			console.log('[Rater][makeDiff] Empty result :');
			console.log(result);
			return;
		}
		var diffTable = $('<table>').append(
			$('<tr>').append(
				$('<th>').attr({'colspan':'2', 'scope':'col'}).text('Latest revision'),
				$('<th>').attr({'colspan':'2', 'scope':'col'}).text('New text')
			),
			result.compare['*']
		);
		madeDiff.resolve(diffTable);
	})
	.fail(madeDiff.reject);
	
	return madeDiff;
};

Page.prototype.makeNewTopSection = function() {
	var self = this;
	
	var wikitext = {
		above: '',
		projects: '',
		below: ''
	};

	if ( self.oldTopSection && self.banners ) {
		var firstBannerIndex = self.banners.reduce(function(currentMinIndex, banner) {
			if ( banner.isNew() ) { 
				return currentMinIndex;
			}
			return Math.min(
				currentMinIndex,
				self.oldTopSection.indexOf(banner.rawWikitext)
			);
		}, Infinity);
		var afterLastBannerIndex = self.banners.reduce(function(currentMaxIndex, banner) {
			if ( banner.isNew() ) { 
				return currentMaxIndex;
			}
			return Math.max(
				currentMaxIndex,
				self.oldTopSection.indexOf(banner.rawWikitext) + banner.rawWikitext.length
			);
		}, -1);
		
		if ( firstBannerIndex >= afterLastBannerIndex ) {
			// TODO: ask for user confirmation
			// Place new banners at end of top section
			wikitext.projects = self.oldTopSection;
		} else {
			wikitext.above = self.oldTopSection.slice(0, firstBannerIndex).trim();
			wikitext.projects = self.oldTopSection.slice(firstBannerIndex, afterLastBannerIndex).trim();
			wikitext.below = self.oldTopSection.slice(afterLastBannerIndex).trim();
		}
		
	}
	
	wikitext.projects = self.banners.reduce(function(newWikitext, banner) {
		// Not touched (and not new, and redirect not bypassed)
		if ( $.isEmptyObject(banner.touched) && !banner.isNew() && !banner.bypassRedirect ) {
			return newWikitext;
		}
		// Marked for removal
		if ( banner.remove ) {
			if ( banner.isNew() ) { return newWikitext; }
			return newWikitext.replace(banner.rawWikitext, '');
		}
		// Existing banner that's been modified
		if ( !banner.isNew() ) {
			return newWikitext.replace(banner.rawWikitext.trim(), banner.buildWikitext());
		}
		// New banner
		return newWikitext += '\n' + banner.buildWikitext();
	}, wikitext.projects);
	
	return (wikitext.above + '\n' + wikitext.projects + '\n' + wikitext.below).trim();
};

Page.prototype.makeEditSummary = function() {
	return this.banners.reduce(function(changes, banner) {
		// Not touched (and not new, and redirect not bypassed)
		if ( $.isEmptyObject(banner.touched) && !banner.isNew() && !banner.bypassRedirect ) {
			return changes;
		}
		// New and removed, no action needed
		if ( banner.remove && banner.isNew() ) {
			return changes;
		}
		
		// Symbol
		var symbol = '';
		if ( banner.remove ) {
			symbol = '−';
		} else if ( banner.isNew() ) {
			symbol = '+';
		}
		// Transclusion name, without WikiProject prefix
		var name = banner.getTransclusionName().replace('WikiProject ','');
		// Ratings, if touched
		var rating = '';
		if ( !banner.remove ) {
			var classRating = ( banner.touched.class ) ? banner.parameters.class.trim() : '';
			var impRating = ( banner.touched.importance ) ? banner.parameters.importance.trim() : '';
			if ( classRating && impRating ) {
				rating = classRating + '/' + impRating;
			} else {
				rating = classRating || impRating || '';
			}
			if ( rating ) {
				rating = ' (' + rating + ')';
			}
		}
		return changes += ' ' + symbol + name + rating + ';';
	}, 'Assessment:').slice(0,-1);
};

Page.prototype.setRedirectsTo = function(pageObjects) {
	var gotRedirects = $.Deferred();
	
	if ( pageObjects == null ) {
		pageObjects = this;
	}
	
	var pageObjectsToCheck = ( $.isArray(pageObjects) ) ? pageObjects : [pageObjects];

	var processRedirects = function(result) {
		if ( !result || !result.query ) {
			gotRedirects.reject();
			return;
		}
		if ( result.query.redirects ) {
			$.each(result.query.redirects, function(_index, redirect) {
				for ( var i=0; i<pageObjectsToCheck.length; i++ ) {
					if ( pageObjectsToCheck[i].getPrefixedText() === redirect.from ) {
						pageObjectsToCheck[i].redirectsTo = Page.newFromText(redirect.to);
						break;
					}
				}
			});
		}
		gotRedirects.resolve();
	};
	
	API.get({
		'action': 'query',
		'format': 'json',
		'titles': pageObjectsToCheck.map(function(pageObj) {
			return pageObj.getPrefixedText();
		}),
		'redirects': 1
	})
	.done( processRedirects )
	.fail( gotRedirects.reject );
	
	return gotRedirects;
};
Page.prototype.setTemplatesBanners = function() {
	var self = this;
	var gotTemplatesBanners = $.Deferred();
	
	if ( self.oldTopSection === '' ) {
		return gotTemplatesBanners.resolve();
	}

	var makeTemplatesObjectsArray = function(wikitext) {
		// Reset lastindex of regex pattern so that test will work
		config.regex.template.lastIndex = 0;
		if ( !config.regex.template.test(wikitext) ) {
			return [];
		}
		return wikitext.match(config.regex.template).map(Template.newFromRawWikitext);
	};
	
	var isShellTemplate = function(templateObject) {
		return -1 !== $.inArray(templateObject.getMainText(), config.shellTemplates);
	};
		
	// Initially finds top-level templates only (not templates within template parameters)
	var talkpageTemplates = makeTemplatesObjectsArray(self.oldTopSection);
	
	// Find sub-templates within WikiProject banner shell
	var shellTemplate = talkpageTemplates.filter(isShellTemplate)[0];
	if ( shellTemplate && shellTemplate.parameters[1] ) {
		var talkpageSubtemplates = makeTemplatesObjectsArray(shellTemplate.parameters[1]);
		// Merge subtemplates into main array
		$.merge(talkpageTemplates, talkpageSubtemplates);
	}

	// Check for redirects, then filter out non-banners
	$.when(self.setRedirectsTo(talkpageTemplates), config.deferred.gotListOfBanners)
	.done(function(){
		var talkpageBanners = talkpageTemplates.filter(function(templateObject) {
			return -1 !== $.inArray(templateObject.getRedirectOrMainText(), config.banners);
		});
		
		self.banners = talkpageBanners;
		gotTemplatesBanners.resolve();
	})
	.fail(function() { gotTemplatesBanners.reject(); });
	return gotTemplatesBanners;
};

/* ========== Template class ==================================================================== */
// Constructor
var Template = function(title, parameters, rawWikitext) {
	try {
		Page.call(this, decodeURIComponent(title));
	} catch(e) {
		throw new Error('Unable to parse template title "'+title+'"'); 
	}
	this.parameters = parameters || {};
	this.rawWikitext = rawWikitext || null;
	this.isProjectBanner = null;
	this.touched = {};
};

// Utility function
/**
 * makeParamsObject
 *
 * Converts a string containg template parameters into an object of parameter names (or positons)
 * and their values.
 *
 * @param {string} wikitext
 *  Wikitext containg parameters
 * @return {object} Object with key:value pairs correspoinding to |parameter=value pairs.
 */
Template.makeParamsObject = function(wikitext) {
	var params = {};
	var unnamedParamCount = 0;
	var parts = wikitext.match(config.regex.templateParams);
	for ( var i=0; i<parts.length; i++ ) {
		if ( parts[i].trim() === '|' ) {
			//Empty unnamed parameter, i.e. {{foo||bar}}
			unnamedParamCount++;
			continue;
		}
		var equalsIndex = parts[i].indexOf('=');
		if ( equalsIndex === -1 ) {
			//unnamed parameter
			unnamedParamCount++;
			params[unnamedParamCount.toString()] = parts[i].slice(1).trim();
		} else {
			params[parts[i].slice(1, equalsIndex).trim()] = parts[i]
				.slice(equalsIndex+1).trim();
		}
	}
	return params;
};

// Constructor from raw wikitext, i.e. `{{TemplateName|value1|para2=value2|para3=value3}}`
Template.newFromRawWikitext = function(rawWikitext) {
	// Reset lastindex of regex pattern so that exec will work
	config.regex.template.lastIndex = 0;
	var parts = config.regex.template.exec(rawWikitext);
	if ( !parts || !parts[0] || !parts[1] ) {
		throw new Error('Unable to parse template from wikitext: ' + rawWikitext);
	}
	var params = ( parts[2] ) ? Template.makeParamsObject(parts[2]) : null;
	return new Template('Template:'+parts[1], params, parts[0]);
};

// ---------- Template prototype ---------------------------------------------------------------- */
// Inherited from Page
Template.prototype = Object.create(Page.prototype);
Template.prototype.constructor = Template;
// Additional functions
Template.prototype.isNew = function() {
	return this.rawWikitext === null;
};
Template.prototype.getParamValue = function(param) {
	return this.parameters[param] || null;
};
Template.prototype.setParamValue = function(param, val) {
	this.parameters[param] = val;
	this.touched[param] = true;
};
Template.prototype.deleteParam = function(param) {
	delete this.parameters[param];
	this.touched[param] = true;
};
Template.prototype.getTransclusionName = function() {
	if ( this.bypassRedirect ) {
		return this.redirectsTo.getMainText();
	}
	return this.getMainText();
};
Template.prototype.getLinkedName = function() {
	if ( this.bypassRedirect ) {
		return extraJs.makeLink(this.redirectsTo.getPrefixedText(), this.redirectsTo.getMainText());
	}
	return extraJs.makeLink(this.getPrefixedText(), this.getMainText());
};

// Banner-specific functions
Template.prototype.parseClassesAndImportances = function() {
	var self = this;
	var parsed = $.Deferred();
	
	if ( self.classes && self.importances ) {
		return parsed.resolve();
	}
	
	var testWikitext = config.bannerDefaults.extendedClasses.map(function(c) {
		return '{{' + self.getMainText() + '|class=' + c + '}}';
	}).join('/n') +
	config.bannerDefaults.extendedImportances.map(function(i) {
		return '{{' + self.getMainText() + '|importane=' + i + '}}';
	}).join('/n');
	
	var processCategories = function(result) {
		var catsHtml = result.parse.categorieshtml['*'];
		self.classes = $.merge(
			$.merge([],	config.bannerDefaults.classes),
			config.bannerDefaults.extendedClasses.filter(function(c) {
				return catsHtml.indexOf(c+'-Class') !== -1;
			})
		);
		self.importances = $.merge(
			$.merge([],	config.bannerDefaults.importances),
			config.bannerDefaults.extendedImportances.filter(function(i) {
				return catsHtml.indexOf(i+'-importance') !== -1;
			})
		);
		parsed.resolve();
	};
		
	
	API.get({
		action: 'parse',
		title: 'Talk:Sandbox',
		text: testWikitext,
		prop: 'categorieshtml'
	}).done(processCategories)
	.fail(parsed.reject);
	
	return parsed;
};

Template.prototype.makeParameterSuggestions = function() {
	var self = this;
	var gotParams = $.Deferred();
	
	if ( self.parameterSuggestions ) { return gotParams.resolve(); }
	
	var processTemplatedata = function(result) {
		// Figure out page id (beacuse action=templatedata doesn't have an indexpageids option)
		var id = $.map(result.pages, function( _value, key ) { return key; });
		
		if ( result.pages[id].notemplatedata || !result.pages[id].paramOrder ) {
			gotParams.reject();
			return;
		}
			
		self.parameterSuggestions = result.pages[id].paramOrder.filter(function(paramName) {
			return ( paramName !== 'class' && paramName !== 'importance' );
		})
		.map(function(paramName) {
			return {data: paramName};
		});
		
		gotParams.resolve();
	};
			
	API.get({
		action: 'templatedata',
		titles: self.getRedirectOrPrefixedText(),
		doNotIgnoreMissingTitles: 1
	})
	.done( processTemplatedata )
	.fail( gotParams.reject );
	return gotParams;
};

Template.prototype.getUnusedParamterSuggestions = function() {
	return this.parameterSuggestions || [];
	/*
	if ( !this.parameterSuggestions ) {
		return [];
	}
	
	return this.parameterSuggestions.filter(function(param) {
		return self.templateObject.parameters[param.data] == null;
	});
	*/
};

Template.prototype.buildWikitext = function() {
	var paras = '';
	if ( !$.isEmptyObject(this.parameters) ) {
		paras = ' ' + $.map(this.parameters, function(val, name) {
			return '|'+name+'='+val;
		}).join(' ');
	}		
	return '{{' + this.getTransclusionName() + paras + '}}';
};
	
/* ========== ComboBoxInputPrompt class ========================================================== */
// Subclass of OOjs UI ProcessDialog 
var ComboBoxInputPrompt = function( config ) {
  ComboBoxInputPrompt.super.call( this, config );
};
OO.inheritClass( ComboBoxInputPrompt, OO.ui.ProcessDialog );

// Specify a name for .addWindows()
ComboBoxInputPrompt.static.name = 'comboBoxInput';
// Specify the static configurations: title and action set
ComboBoxInputPrompt.static.actions = [
  { flags: 'primary', label: 'Add', action: 'add' },
  { flags: 'safe', label: 'Cancel' }
];

// Customize the initialize() function to add content and layouts: 
ComboBoxInputPrompt.prototype.initialize = function () {
	ComboBoxInputPrompt.super.prototype.initialize.call( this );
	this.panel = new OO.ui.PanelLayout( { padded: true, expanded: false } );
	this.content = new OO.ui.FieldsetLayout();

	this.input = new OO.ui.ComboBoxInputWidget( {
		$overlay: this.$overlay,
		//validate: 'non-empty',
		//autofocus: true,
		menu: {
			filterFromInput: true
		}
	});

	this.input.$pending.addClass( 'oo-ui-pendingElement-pending' );

	this.field = new OO.ui.FieldLayout( this.input, {
		label: ' ',//placeholder
		align: 'top'
	} );

	this.content.addItems([ this.field ]);
	this.panel.$element.append( this.content.$element );

	this.$body.append( this.panel.$element );

	this.input.connect( this, { 'change': 'onInputChange' } );
	this.input.connect( this, { 'enter': 'onEnterPress' } );
};

// Specify any additional functionality required by the window (disable opening an empty URL, in this case)
ComboBoxInputPrompt.prototype.onInputChange = function ( value ) {
  this.actions.setAbilities( {
    add: !!value.length 
  } );
};
ComboBoxInputPrompt.prototype.onEnterPress = function() {
	this.executeAction('add');
};


// Specify the dialog height (or don't to use the automatically generated height).

ComboBoxInputPrompt.prototype.getBodyHeight = function () {
  return this.panel.$element.outerHeight( true )*1.1;
};


// Use getSetupProcess() to set up the window with data passed to it at the time of opening.
ComboBoxInputPrompt.prototype.getSetupProcess = function ( data ) {
	var self = this;
		
	data = data || {};
	return ComboBoxInputPrompt.super.prototype.getSetupProcess.call( this, data )
	.next( function () {
		if ( data.label ) {
			self.field.setLabel(data.label);
		}
		if ( data.help ) {
			self.field.setNotices([data.help]);
		}

		// Set up contents based on data
		$.when( data.optionsReady )
		.done(function() {
			self.input.setOptions(
				data.makeOptions()
			);
		})
		.fail(function() {
			if ( data.failnotice ) {
				self.field.setNotices(
					( data.help ) ? [data.help, data.failnotice] : [data.failnotice]
				);
				self.updateSize();
			}
		})
		.always(function() {
			self.input.$pending.removeClass( 'oo-ui-pendingElement-pending' );
		});
  }, this );
};

// Specify processes to handle the actions.
ComboBoxInputPrompt.prototype.getActionProcess = function ( action ) {
	var self = this;
	if ( action === 'add' ) {
		// Close dialog, passing through the input data
		return new OO.ui.Process( function () {
			self.close({input: self.input.getValue()});
		});
	}
	// Fallback to parent handler
	return ComboBoxInputPrompt.super.prototype.getActionProcess.call( this, action );
};

// Use the getTeardownProcess() method to perform actions whenever the dialog is closed. 
// This method provides access to data passed into the window's close() method 
// or the window manager's closeWindow() method.
ComboBoxInputPrompt.prototype.getTeardownProcess = function ( data ) {
  return ComboBoxInputPrompt.super.prototype.getTeardownProcess.call( this, data );
//  .first( function () {
//  // Perform any cleanup as needed
//  }, this );
};
	

/* ========== Dialog class ====================================================================== */
// Constructor
var Dialog = function(currentPage) {
	this.page = currentPage;
	
	// Make an new dialog/interface window	
	this.interfaceWindow = new Morebits.simpleWindow(
		Math.min(900, Math.floor(window.innerWidth*0.8)),
		Math.floor(window.innerHeight*0.9)
	);
	this.interfaceWindow.setTitle('Rater [v.'+config.script.version+']');
	this.interfaceWindow.addFooterLink('script documentation', 'WP:RATER');
	this.interfaceWindow.addFooterLink('feedback', 'WT:RATER');
	this.interfaceWindow.setContent(
		$('<div>')
		.attr('id', 'rater-dialog')
		.append(
			//$('<div>').attr('id', 'rater-dialog-header'),
			$('<div>').attr('id', 'rater-dialog-body')
		)
		.get(0)
	);
	$('a.ui-dialog-titlebar-close.ui-corner-all').remove();
	$('#rater-dialog').parent().css('background-color', '#f0f0f0');
	this.$footerButtons = $('#rater-dialog').parent().nextAll('.ui-dialog-buttonpane')
		.find('span.morebits-dialog-buttons');
	this.interfaceWindow.display();
};
// Overlay dialog (for previews, diffs, etc)
Dialog.showOverlayDialog = function(contentDeferred, heading) {
	var overlayDialog = new OO.ui.MessageDialog();
	config.windowManager.addWindows( [ overlayDialog ] );
	var instance = config.windowManager.openWindow( overlayDialog, {
		title: heading,
		message: 'Loading...',
		size: 'larger',
		actions: [ {
			action: 'accept',
			label: 'Close',
			flags: 'primary'
		} ]
	} );
	instance.opened.then( function() {
		contentDeferred.done(function(contentHtml){
			overlayDialog.$element.find('label.oo-ui-messageDialog-message').empty().after(contentHtml);
			overlayDialog.updateSize();
		})
		.fail(function(code, jqxhr) {
			overlayDialog.$element.find('label.oo-ui-messageDialog-message').empty().append(
				heading + ' failed.',
				( code == null ) ? '' : ' ' + extraJs.makeErrorMsg(code, jqxhr)
			);
		});
	});
	instance.closed.then(function(){ config.windowManager.clearWindows(); });
};

// ---------- Dialog prototype ------------------------------------------------------------------ */

// --- Basic manipulation: ---
// Append content to header
//Dialog.prototype.addToHeader = function($content) {
//	$('#rater-dialog-header').append($content);
//};
// Append content to body
Dialog.prototype.addToBody = function($content) {
	$('#rater-dialog-body').append($content);
};
// Add buttons to footer
Dialog.prototype.setFooterButtons = function($buttons, mode) {
	if ( mode === 'prepend' ) {
		this.$footerButtons.prepend($buttons);
	} else if ( mode === 'append' ) {
		this.$footerButtons.append($buttons);
	} else {
		this.$footerButtons.empty().append($buttons);
	}
};
// Clear dialog
Dialog.prototype.emptyContent = function() {
	$('#rater-dialog-body').empty();
};
// Display dialog
Dialog.prototype.display = function() {
	this.interfaceWindow.display();
};
// Reset height
Dialog.prototype.resetHeight = function() {
	this.interfaceWindow.setHeight(Math.floor(window.innerHeight*0.9));
};
// Close dialog
Dialog.prototype.close = function() {
	this.interfaceWindow.close();
};

// --- Make interface elements: ---
Dialog.icons = {
	'delete': 	{
		'source':	'/media/wikipedia/commons/thumb/1/18/OOjs_UI_icon_close-ltr.svg/40px-OOjs_UI_icon_close-ltr.svg.png',
		'tooltip':	'Remove template'
	},
	'clear':	{
		'source':	'/media/wikipedia/commons/thumb/5/54/OOjs_UI_icon_noWikiText-ltr.svg/40px-OOjs_UI_icon_noWikiText-ltr.svg.png',
		'tooltip':	'Clear parameters'
	},
	'bypass':	{
		'source':	'/media/wikipedia/commons/thumb/5/5d/OOjs_UI_icon_newline-rtl.svg/40px-OOjs_UI_icon_newline-rtl.svg.png',
		'tooltip':	'Bypass redirect'
	},
	'ores':		{
		'source':	'/media/wikipedia/commons/thumb/5/51/Objective_Revision_Evaluation_Service_logo.svg/40px-Objective_Revision_Evaluation_Service_logo.svg.png',
		'tooltip':	'Machine-predicted quality from ORES'
	}
};

Dialog.makeIcon = function(iconName, clickable) {
	return $('<img>').attr({
		'src':		Dialog.icons[iconName].source,
		'title':	Dialog.icons[iconName].tooltip,
		'alt':		iconName,
		'width':	'20px',
		'height':	'20px'
	}).addClass('rater-dialog-'+iconName)
	.css( (clickable===false) ? {} : {'float':'left', 'cursor':'pointer', 'margin-right':'0.2em'});
};

Dialog.makeDropdown = function(values, selectedValue) {
	var $dropdown = $('<select>')
	.addClass('rater-dialog-dropdown')
	.append(
		values.map(function(val) {
			return $('<option>').attr('value', val.toLowerCase()).text(val);
		})
	);
	if ( selectedValue != null ) {
		$dropdown.children("[value='"+selectedValue.toLowerCase()+"']").attr('selected', 'selected');
	}
	return $dropdown;
};

Dialog.makeParamInput = function(currentVal, param) {
	return $('<span>').addClass('rater-dialog-paraInput').append(
		$('<label>').text(param),
		$('<input>').attr('type', 'text').val(currentVal),
		$('<a>').attr('title', 'remove').text('x'),
		$('<wbr>')
	);
};

Dialog.makeButton = function(options) {
	var $button = new OO.ui.ButtonWidget(options).$element;
	$button.children().css({'padding-top':'0.5em','padding-bottom':'0.4em'});
	return $button;
};
Dialog.makeFramelessButton = function(options) {
	options.framed = false;
	var $button = new OO.ui.ButtonWidget(options).$element;
	$button.css('padding','0')
		.children().css({'padding-top':'0.3em','padding-bottom':'0.3em'})
			.children().css('font-weight','normal');
	return $button;
};

Dialog.prototype.makeRow = function(templateObject) {
	var self = this;
	var $row = $('<div>');

	var setParamHandlers = function() {
		var $span = $(this);
		var $label = $span.children('label');
		var $input = $span.children('input');
		var $a = $span.children('a');
		$input.blur(function() {
			if ( templateObject.parameters[$label.text()] !== $input.val().trim() ) {
				templateObject.setParamValue($label.text(), $input.val().trim());
			}
		});
		$a.click(function() {
			templateObject.deleteParam($label.text());
			self.rebuildRow($row, templateObject);
		});
	};
	
	var removeTemplate = Dialog.makeIcon('delete').click(function() {
		templateObject.remove = true;
		templateObject.isProjectBanner = false;
		$row.remove();
	});
	
	var clearTemplate = Dialog.makeIcon('clear').click(function() {
		$.each(templateObject.parameters, function(paraName) {
			templateObject.setParamValue(paraName, null);
		});
		self.rebuildRow($row, templateObject);
	});
	
	var templateName = $('<span>').addClass('rater-dialog-templateName').append(
		'{{',
		templateObject.getLinkedName(),
		'}}'
	);

	var bypassRedirect = '';
	if ( templateObject.redirectsTo && !templateObject.bypassRedirect ) {
		bypassRedirect = Dialog.makeIcon('bypass').click(function() {
			templateObject.bypassRedirect = true;
			templateName.empty().append(
				'{{',
				templateObject.getLinkedName(),
				'}}'
			);
			$(this).remove();
		});
	}
	
	var classDropdown = Dialog.makeDropdown(
		templateObject.classes,
		templateObject.parameters.class || templateObject.parameters.Class
	).change(function() {
		var newValue = $(this).val();
		if (
			templateObject.parameters.class && ( newValue.toLowerCase() === templateObject.parameters.class.toLowerCase() )||
			templateObject.parameters.Class && ( newValue.toLowerCase() === templateObject.parameters.Class.toLowerCase() )
		) {
			return;
		}
		
		templateObject.setParamValue('class', newValue);
		if ( templateObject.parameters.Class ) {
			templateObject.parameters.Class = null;
		}
	});
	
	var importanceDropdown = Dialog.makeDropdown(
		templateObject.importances,
		templateObject.parameters.importance || templateObject.parameters.Importance
	).change(function() {
		var newValue = $(this).val();
		if (
			templateObject.parameters.importance && ( newValue.toLowerCase() === templateObject.parameters.importance.toLowerCase() ) ||
			templateObject.parameters.Importance && ( newValue.toLowerCase() === templateObject.parameters.Importance.toLowerCase() )
		) {
			return;
		}
		
		templateObject.setParamValue('importance', newValue);
		if ( templateObject.parameters.Class ) {
			templateObject.parameters.Class = null;
		}
	});

	var listasParam = '';
	if ( templateObject.getRedirectOrMainText() === 'WikiProject Biography' ) {
		if ( templateObject.parameters.listas ) {
			listasParam = Dialog.makeParamInput(templateObject.parameters.listas, 'listas');
		} else {
			listasParam = Dialog.makeParamInput(self.page.getListasAutofill(), 'listas')
			.addClass('rater-dialog-autofill')
			.on('keypress change', function(){
				$(this).removeClass('rater-dialog-autofill').off('keypress change');
			});
		}
		listasParam.find('a').hide();
		listasParam.each(setParamHandlers);
	}

	var addParam = $('<span>').append(
		Dialog.makeFramelessButton({
			label:	'[add parameter]',
			icon:	'tableAddColumnBefore'
		})
	);
	
	addParam.click(function() {		
		var sugesstionsReady = templateObject.makeParameterSuggestions();
		var prompt = new ComboBoxInputPrompt();
		config.windowManager.addWindows( [ prompt ] );
		
		instance = config.windowManager.openWindow( prompt, {
			label: 'Add parameter',
			optionsReady: sugesstionsReady,
			makeOptions: function() {
				return templateObject.getUnusedParamterSuggestions();
			},
			failnotice: new OO.ui.HtmlSnippet(
				$('<span>').css({'color':'#555', 'font-size':'92%'}).append(
					'Parameter suggestions not available.',
					$('<p>').append(
						'This WikiProject banner has not been configured for use with this tool. See the ',
						extraJs.makeLink('User:Evad37/rater#TemplateData_quick_tutorial', 'TemplateData quick tutorial'),
						' or ask for help on ',
						extraJs.makeLink('User talk:Evad37/rater.js', ' the script\'s talk page'),
						'.'
					)
				)
			)
		} );
		instance.opened.then( function() {
			prompt.input.focus();
		});
		instance.closed.then( function ( data ) {
			config.windowManager.clearWindows();
			if ( !data || !data.input ) {
				// No input data - ie cancelled
				return;
			}
			if ( templateObject.parameters[data.input] != null ) {
				alert('There is already a ' + data.input + ' parameter!');
				return;
			}
			templateObject.parameters[data.input] = '';
			self.rebuildRow($row, templateObject);
		} );
	});
	
	var otherParams = $('<div>').append(
		$.map(templateObject.parameters, function(val, param) {
			if (
				val === null ||
				param.toLowerCase() === 'class' ||
				param.toLowerCase() === 'importance' ||
				( param === 'listas' &&
				  templateObject.getRedirectOrMainText() === 'WikiProject Biography' )				
			) {
				return '';
			}
			return Dialog.makeParamInput(val, param);
		}),
		addParam
	);
	otherParams.children('span').each(setParamHandlers);
	
	//var moreless = $('<a>').addClass('rater-dialog-moreless').text('[+/-]').click(function() {
	//	otherParams.toggle();
	//);
	
	return $row.addClass('rater-dialog-row').append(
		$('<div>').append(
			removeTemplate,
			clearTemplate,
			templateName,
			bypassRedirect,
			$('<span>').addClass('rater-dialog-row-class').append(
				'Class:',
				classDropdown
			),
			$('<span>').addClass('rater-dialog-row-importance').append(
				'Importance:',
				importanceDropdown
			),
			listasParam
			//moreless
		),
		otherParams
	);
};

Dialog.prototype.makeActionsRow = function() {
	var self = this;
	
	var addProject = Dialog.makeButton({
		label:	'Add WikiProject',
		icon:	'add',
		flags:	['progressive']
	})
	.click(function() {
		var prompt = new ComboBoxInputPrompt();
		config.windowManager.addWindows( [ prompt ] );
		instance = config.windowManager.openWindow( prompt, {
			label: 'Add Template:',
			optionsReady: config.deferred.gotListOfBanners,
			makeOptions: function() { return config.bannerOptions; }
		} );
		instance.opened.then( function() {
			prompt.input.focus();
		});
		instance.closed.then( function ( data ) {
			config.windowManager.clearWindows();
			
			if ( !data || !data.input ) { return; }
			
			var existingBanner = self.page.getBannerFromNameOrRedirect(data.input);
			
			if ( existingBanner && !existingBanner.remove ) {
				alert('There is already a {{' + existingBanner.getTransclusionName() + '}} banner!');
				return;
			}
			
			var templateObject;
			
			if ( existingBanner ) {
				existingBanner.remove = false;
				existingBanner.parameters = {};
				templateObject = existingBanner;
			} else {
				templateObject = new Template('Template:'+data.input);
				templateObject.isProjectBanner = true;
				self.page.banners.push(templateObject);
			}

			$.when(
				!!existingBanner || templateObject.setRedirectsTo(),
				!!existingBanner || templateObject.parseClassesAndImportances()
			).then( function() {
				var newRow = self.makeRow(templateObject).insertBefore('#rater-dialog-actions');
				self.autofillClassAndImportance(newRow);
			} );
		} );
	});
	
	var removeAll = Dialog.makeButton({
		label:	'Remove all',
		icon:	'close',
		flags:	['destructive']
	}).click(function() {
		$.each(self.page.banners, function(_index, templateObject) {
			templateObject.remove = true;
		});
		$('div.rater-dialog-row').not('#rater-dialog-actions').remove();
	});
	
	var clearAll = Dialog.makeButton({
		label:	'Clear all',
		icon:	'noWikiText'
	}).click(function() {
		$.each(self.page.banners, function(_index, templateObject) {
			$.each(templateObject.parameters, function(paraName) {
				templateObject.setParamValue(paraName, null);
			});
		});
		self.refresh();
	});

	var bypassAllRedirects = '';
	if ( $('img.rater-dialog-bypass').length ) {
		bypassAllRedirects = Dialog.makeButton({
			label:	'Bypass redirects',
			icon:	'arrowNext'
		}).click(function() {
			$.each(self.page.banners, function(_index, templateObject) {
				if ( templateObject.redirectsTo && !templateObject.bypassRedirect ) {
					templateObject.bypassRedirect = true;
				}
			});
			self.refresh();
		});
	}
	
	var classForAllDropdown = Dialog.makeDropdown(config.bannerDefaults.classes).change(function() {
		var newValue = $(this).val();
		$('span.rater-dialog-row-class > select').val(newValue).change();
	}).prepend($('<option>').attr('disabled','disabled').text('Class'));
	
	var importanceForAllDropdown = Dialog.makeDropdown(config.bannerDefaults.importances).change(function() {
		var newValue = $(this).val();
		$('span.rater-dialog-row-importance > select').val(newValue).change();
	}).prepend($('<option>').attr('disabled','disabled').text('Importance'));
	
	var setAll = Dialog.makeButton({
		label:	'Set all',
		icon:	'tag'
	});
	setAll.find('span.oo-ui-labelElement-label').append(
		classForAllDropdown,
		importanceForAllDropdown
	);
	
	var oresPrediction = '';
	if ( self.page.oresScore ) {
		oresPrediction = $('<div>').append(
			Dialog.makeIcon('ores', false),
			'&nbsp;',
			extraJs.makeLink('mw:ORES', 'ORES'),
			' Predicted class: ',
			$('<b>').text(self.page.oresScore)
		);
	}
	
	return $('<div>').attr('id', 'rater-dialog-actions').addClass('rater-dialog-row').append(
		oresPrediction,
		$('<div>').append(
			addProject,
			removeAll,
			clearAll,
			bypassAllRedirects,
			setAll
		)
	);
};

Dialog.prototype.makeButtons = function() {
	if ( this.$footerButtons.children().length !== 0 ) {
		return;
	}
	
	var self = this;
	
	var cancel = Dialog.makeButton({
		label:	'Cancel',
		framed:	false,
		flags:	['destructive']
	}).click(function() { self.close(); });
	
	var save = Dialog.makeButton({
		label:	'Save changes',
		flags:	['primary', 'progressive']
	}).click(function() { self.onSaveClick(); });
	
	var preview = Dialog.makeButton({
		label:	'Show preview'
	}).click(function() {
		Dialog.showOverlayDialog( self.page.makePreview(), 'Preview' );
	});

	var showdiff = Dialog.makeButton({
		label:	'Show changes'
	}).click(function() {
		Dialog.showOverlayDialog( self.page.makeDiff(), 'Diff' ); 
	});
	
	self.setFooterButtons([save, preview, showdiff, cancel]);
};
Dialog.prototype.onSaveClick = function() {
	var self = this;

	var close = Dialog.makeButton({
		label:	'Close'
	}).click(function() { self.close(); });
	
	self.emptyContent();
	self.setFooterButtons(null);
	self.addToBody('Saving...');
	
	$.when( self.page.makeEdit() )
	.done( function() {
		self.addToBody('Done!');
	})
	.fail( function(code, jqxhr) {
		self.addToBody('Failed. ' + extraJs.makeErrorMsg(code, jqxhr));
	} )
	.always(function() {
		self.setFooterButtons(close);
	});
};

Dialog.prototype.autofillClassAndImportance = function($rowDiv) {
	var self = this;
	var $top = $rowDiv || $('#rater-dialog-body');
	
	// Autofill empty classes (if possible)
	var extrapolateClassFromExisting = function() {
		var classes = $('span.rater-dialog-row-class > select').map(function() {
			return $(this).val();
		}).get().sort();
		if ( -1 === $.inArray(' ', classes) ) {
			return false;
		}
		return classes.filter(function(c) {
			return c!== ' ';
		})[0];
	};
	
	var extrapolateClassFromOres = function() {
		if ( !self.page.oresScore ) {
			return null;
		}
		return ( self.page.oresScore.toLowerCase() === 'stub' ) ? 'stub' : 'start';
	};
	
	var extrapolated = extrapolateClassFromExisting() || extrapolateClassFromOres();
	
	if ( extrapolated ) {
		$top.find('span.rater-dialog-row-class > select').each(function() {
			var $this = $(this);
			if ( $this.val() === ' ' ) {
				$this.val(extrapolated).change();
				$this.parent().addClass('rater-dialog-autofill')
				.on('keypress change', function(){
					$(this).removeClass('rater-dialog-autofill').off('keypress change');
				});
			}
		});
	}
	
	// Autofill empty importances to 'low' (articles only)
	if ( config.mw.wgNamespaceNumber <= 1 ) {
		$top.find('span.rater-dialog-row-importance > select').each(function() {
			var $this = $(this);
			if ( $this.val() === ' ' ) {
				$this.val('low').change();
				$this.parent().addClass('rater-dialog-autofill')
				.on('keypress change', function(){
					$(this).removeClass('rater-dialog-autofill').off('keypress change');
				});
			}
		});
	}
};

Dialog.prototype.build = function(isInitialBuild) {
	var self = this;
	var isBuilt = $.Deferred();
	
	var parseBanners = '';
	if ( self.page.banners ) {
		parseBanners = self.page.banners.map(function(banner) {
			return banner.parseClassesAndImportances();
		});
	}
	
	$.when.apply(null, parseBanners).then(function() {
		if ( self.page.banners ) {
			self.addToBody(
				self.page.banners.map(function(banner) {
					if ( banner.remove ) {
						return '';
					}
					return self.makeRow(banner);
				})
			);
		}
		
		self.addToBody(self.makeActionsRow());
		
		if ( isInitialBuild && self.page.banners.length>0 ) {
			self.autofillClassAndImportance();			
		}

		self.makeButtons();
		self.resetHeight();
		
		isBuilt.resolve();
	});
	return isBuilt;
};
	
Dialog.prototype.refresh = function() {
	this.emptyContent();
	this.build();
};

Dialog.prototype.rebuildRow = function(rowDiv, banner) {
	this.makeRow(banner).insertAfter(rowDiv);
	rowDiv.remove();
};

/* ========== ============ ====================================================================== */

var getListOfProjectBanners = function() {
	config.deferred.gotListOfBanners = $.Deferred();
	
	var bannerNames = [];
	
	var processQuery = function(result) {
		if ( !result.query || !result.query.categorymembers ) {
			// No results
			
			// TODO: error or warning ********
			config.deferred.gotListOfBanners.reject();
			
			return;
		}
		
		// Gather titles into array - excluding "Template:" prefix
		var resultTitles = result.query.categorymembers.map(function(info) {
			return info.title.slice(9);
		});
		Array.prototype.push.apply(bannerNames, resultTitles);
		
		// Continue query if needed
		if ( result.continue ) {
			doApiQuery($.extend(query, result.continue));
			return;
		}
		
		config.banners = bannerNames;
		config.deferred.gotListOfBanners.resolve();
		
	};

	var query = {
		action: 'query',
		format: 'json',
		list: 'categorymembers',
		cmtitle: 'Category:WikiProject banners with quality assessment',
		cmprop: 'title',
		cmnamespace: '10',
		cmlimit: '500'
	};
	
	var doApiQuery = function(q) {
		API.get( q )
		.done( processQuery )
		.fail( function(code, jqxhr) {
			console.warn('[Rater] ' + extraJs.makeErrorMsg(code, jqxhr, 'Could not retrieve pages from [Category:WikiProject banners with quality assessment]'));
			config.deferred.gotListOfBanners.reject();
		} );
	};
	
	doApiQuery(query);
	return config.deferred.gotListOfBanners;
};

var makeBannerOptions = function() {
	config.deferred.gotBannerOptions = $.Deferred();
	config.bannerOptions = config.banners.map(function(bannerName) {
		return {
			data:  bannerName,
			label: bannerName.replace('WikiProject ', '')
		};
	});
};

/* ========== Set up current page and dialog ==================================================== */	
var currentPage = Page.newFromText(config.mw.wgPageName);
var dialog = new Dialog(currentPage);
dialog.addToBody(
	$('<div>').attr('id', 'dialog-loading').append(
		$('<p>').attr('id', 'dialog-loading-0').css('font-weight', 'bold').text('Initialising:'),
		$('<p>').attr('id', 'dialog-loading-1').text('Loading talkpage wikitext...'),
		$('<p>').attr('id', 'dialog-loading-2').text('Parsing talkpage templates...'),
		$('<p>').attr('id', 'dialog-loading-3').text('Retrieving quality prediction...').hide(),
		$('<p>').attr('id', 'dialog-loading-4').text('Building interface...')
	)
);
var showTaskDone = function(taskNumber) {
	$('#dialog-loading-'+taskNumber).append(' Done!');
};
var showTaskFailed = function(taskNumber, code, jqxhr) {
	$('#dialog-loading-'+taskNumber).append(
		' Failed.',
		( code == null ) ? '' : ' ' + extraJs.makeErrorMsg(code, jqxhr)
	);
};


// Load and parse talk page
var talkDeferred = currentPage.getTalkpageTopSection()
.done(function() { showTaskDone(1); })
.fail(function(code, jqxhr) { showTaskFailed(1, code, jqxhr); })
.then(function(){
	return currentPage.setTemplatesBanners()
	.done(function() { showTaskDone(2); })
	.fail(function(code, jqxhr) { showTaskFailed(2, code, jqxhr); });
});

// Retrieve rating from ORES
var oresDeferred = '';
if ( config.mw.wgNamespaceNumber <= 1 ) {
	$('#dialog-loading-3').show();
	oresDeferred = currentPage.getOresScore()
		.done(function() { showTaskDone(3); })
		.fail(function(code, jqxhr) { showTaskFailed(3, code, jqxhr); });
}

// Build dialog
$.when(oresDeferred, talkDeferred).then(function() { dialog.build(true); })
.done(function(){
	showTaskDone(4);
	$('#dialog-loading').hide(600);
})
.fail(function(code, jqxhr) { showTaskFailed(4, code, jqxhr); });

// Get list of all projects (but don't wait for this before displaying dialog)
getListOfProjectBanners().done( makeBannerOptions );
// Make OOjs UI window manager
config.windowManager = new OO.ui.WindowManager();
// - place above Morebits SimpleWindow, which has z-index of ~1000
config.windowManager.$element.css('z-index', '2000');
$( 'body' ).append( config.windowManager.$element );

/* ==========  End of file closure wrappers ===================================================== */
});
});
});
});