User:GreatSculptorIthas/raterfork.js

/*************************************************************************************************** Rater --- by Evad37 > Helps assess WikiProject banners. // $( 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: ' (Rater)',	version: '1.2.2' }; // MediaWiki configuration values config.mw = mw.config.get( [ 'skin', 'wgPageName', 'wgNamespaceNumber', 'wgUserName', 'wgFormattedNamespaces', 'wgMonthNames', 'wgRevisionId', 'wgScriptPath', 'wgServer', 'wgCategories', 'wgIsMainPage' ] ); // 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; } config.regex = {	// Pattern to find templates, which may contain other templates	template:		/\{\{\s*([^#\{\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: [		'Top',		'High',		'Mid',		'Low',		'Bottom',		'NA'	] }; config.customClasses = { 'WikiProject Military history': [ 'AL', 'BL', 'CL' ],	'WikiProject Portals': [ 'FPo', 'Complete', 'Substantial', 'Basic', 'Incomplete', 'Meta' ] }; 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', 'WikiProject banner' ]; config.defaultParameterData = { "auto": { "label": { "en": "Auto-rated" },		"description": { "en": "Automatically rated by a bot. Allowed values: ['yes']." },		"autovalue": "yes" },	"listas": { "label": { "en": "List as" },		"description": { "en": "Sortkey for talk page" }	},	"small": { "label": { "en": "Small?", },		"description": { "en": "Display a small version. Allowed values: ['yes']." },		"autovalue": "yes" },	"attention": { "label": { "en": "Attention required?", },		"description": { "en": "Immediate attention required. Allowed values: ['yes']." },		"autovalue": "yes" },	"needs-image": { "label": { "en": "Needs image?", },		"description": { "en": "Request that an image or photograph of the subject be added to the article. Allowed values: ['yes']." },		"aliases": [ "needs-photo" ],		"autovalue": "yes", "suggested": true },	"needs-infobox": { "label": { "en": "Needs infobox?", },		"description": { "en": "Request that an infobox be added to the article. Allowed values: ['yes']." },		"aliases": [ "needs-photo" ],		"autovalue": "yes", "suggested": true } };

/* ========== Load dependencies ================================================================= */ // 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'); } // Load resoucre loader modules mw.loader.using( ['mediawiki.util', 'mediawiki.api', 'mediawiki.Title', 'mediawiki.util',	'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows', 'jquery.ui'], function {

/* ========== CSS =============================================================================== */ // TODO: convert to .css subpage and load using importStylesheet // Attribution: Diff styles from  mw.util.addCSS(	/* - Main dialog styles --- */	'#dialog-loading .oo-ui-progressBarWidget, #dialog-loading .oo-ui-progressBarWidget > div { max-width: 100%; height: 1.5em; }'+	'.rater-dialog-row { padding:0.2em; border-bottom: 1px solid #777; }'+	'.rater-dialog-row:nth-child(even) { background-color:#e0e0e0; }'+	'.rater-dialog-row > div:nth-child(2) { clear:left; }'+	'.rater-dialog-row > div > span { padding-right:0.5em; white-space:nowrap; }'+	'.rater-dialog-para-label { cursor:help; padding-right:0; }'+	'.rater-dialog-para-code { font-family:monospace; font-size:123%; padding-right:0; }'+	'.rater-dialog-para-code::before { content:"|"; }'+	'.rater-dialog-para-code::after { content:"="; }'+	'.rater-dialog-dropdown { width:5em; margin:0 0.2em; }'+	'.rater-dialog-textInputContainer 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%; }'+ /* - OOjs UI windows -- */ // Need to be shown above Morebits SimpleWindow, which has z-index of ~1000 '.rater-oouiWindowManager, .rater-oouiWindowManager > div { z-index:2000 !important; }'+ '.oo-ui-window.oo-ui-dialog.oo-ui-messageDialog.oo-ui-window-active.oo-ui-window-setup.oo-ui-window-ready { z-index: 3000; }'+ /* - 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;'+ '}' );

/* ========== Utility functions ================================================================= */ /** writeToCache * @param {String} key * @param {Array|Object} val * @param {Number} staleDays Number of days after which the data becomes stale (usable, but should * be updated for next time). * @param {Number} expiryDays Number of days after which the cached data may be deleted. */ var writeToCache = function(key, val, staleDays, expiryDays) { try { var defaultStaleDays = 1; var defaultExpiryDays = 30; var millisecondsPerDay = 24*60*60*1000;

var staleDuration = (staleDays || defaultStaleDays)*millisecondsPerDay; var expiryDuration = (expiryDays || defaultExpiryDays)*millisecondsPerDay; var stringVal = JSON.stringify({			value: val,			staleDate: new Date(Date.now + staleDuration).toISOString,           expiryDate: new Date(Date.now + expiryDuration).toISOString		}); localStorage.setItem('Rater-'+key, stringVal); } catch(e) {} }; /** readFromCache * @param {String} key * @returns {Array|Object|String|Null} Cached array or object, or empty string if not yet cached, *         or null if there was error. */ var readFromCache = function(key) { var val; try { var stringVal = localStorage.getItem('Rater-'+key); if ( stringVal !== '' ) { val = JSON.parse(stringVal); }	} catch(e) { console.log('[Rater] error reading ' + key + ' from localStorage cache:'); console.log(			'\t' + e.name + ' message: ' + e.message +			( e.at ? ' at: ' + e.at : '') +			( e.text ? ' text: ' + e.text : '')		); }	return val || null; }; var isAfterDate = function(dateString) { return new Date(dateString) < new Date; }; var clearCacheItemIfInvalid = function(key) { var isRaterKey = key.indexOf('Rater-') === 0; if ( !isRaterKey ) { return; }   var item = readFromCache(key.replace('Rater-','')); var isInvalid = !item || !item.expiryDate || isAfterDate(item.expiryDate); if ( isInvalid ) { localStorage.removeItem(key); } }; var clearInvalidCacheItems = function { for (var i = 0; i < localStorage.length; i++) { setTimeout(clearCacheItemIfInvalid, 100, localStorage.key(i)); } }; /* ========== 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); }; /* -- Raw wikitext -- */ API.getRaw = function(page) { var gotRaw = $.Deferred; var request = $.get('https:' + config.mw.wgServer + mw.util.getUrl(page, {action:'raw'})) .done(function(data) {		if ( !data ) {			gotRaw.reject('ok-but-empty');			return;		}		gotRaw.resolve(data);	}) .fail(function{		status = request.getResponseHeader('status');		gotRaw.reject('http', {textstatus: status || 'unknown'});	}); return gotRaw; };

/* ========== Additional config & set up ======================================================== */ // Get list of banners var getListOfBannersFromApi = function {

var finishedPromise = $.Deferred;

var querySkeleton = { action: 'query', format: 'json', list: 'categorymembers', cmprop: 'title', cmnamespace: '10', cmlimit: '500' };

var categories = [ {			title:' Category:WikiProject banners with quality assessment', abbreviation: 'withRatings', banners: [], processed: $.Deferred },		{			title: 'Category:WikiProject banners without quality assessment', abbreviation: 'withoutRatings', banners: [], processed: $.Deferred },		{			title: 'Category:WikiProject banner wrapper templates', abbreviation: 'wrappers', banners: [], processed: $.Deferred }	];

var processQuery = function(result, catIndex) { if ( !result.query || !result.query.categorymembers ) { // No results // TODO: error or warning ******** finishedPromise.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(categories[catIndex].banners, resultTitles); // Continue query if needed if ( result.continue ) { doApiQuery($.extend(categories[catIndex].query, result.continue), catIndex); return; }		categories[catIndex].processed.resolve; };

var doApiQuery = function(q, catIndex) { API.get( q ) .done( function(result) {			processQuery(result, catIndex);		} ) .fail( function(code, jqxhr) {			console.warn('[Rater] ' + extraJs.makeErrorMsg(code, jqxhr, 'Could not retrieve pages from ' + q.cmtitle + ''));			finishedPromise.reject;		} ); };	categories.forEach(function(cat, index, arr) {		cat.query = $.extend( { 'cmtitle':cat.title }, querySkeleton );		$.when( arr[index-1] && arr[index-1].processed || true ).then(function{ doApiQuery(cat.query, index); });	});	categories[categories.length-1].processed.then(function{		var banners = {};		var stashBanner = function(catObject) {			banners[catObject.abbreviation] = catObject.banners;		};		var mergeBanners = function(mergeIntoThisArray, catObject) {			return $.merge(mergeIntoThisArray, catObject.banners);		};		var makeOption = function(bannerName) {			var isWrapper = ( -1 !== $.inArray(bannerName, categories[2].banners) );			return {				data: ( isWrapper ? 'subst:' : ) + bannerName,				label: bannerName.replace('WikiProject ', ) + ( isWrapper ? ' [template wrapper]' : '')			};		};		categories.forEach(stashBanner);		var bannerOptions = categories.reduce(mergeBanners, []).map(makeOption);		config.banners = banners;		config.bannerOptions = bannerOptions;		writeToCache('banners', banners, 2, 60);		writeToCache('bannerOptions', bannerOptions, 2, 60);		finishedPromise.resolve;	}); return finishedPromise; }; var getBannersFromCache = function { var cachedBanners = readFromCache('banners'); var cachedBannerOptions = readFromCache('bannerOptions'); if (		!cachedBanners ||		!cachedBanners.value || !cachedBanners.staleDate ||		!cachedBannerOptions ||		!cachedBannerOptions.value || !cachedBannerOptions.staleDate	) { return $.Deferred.reject; }	if ( isAfterDate(cachedBanners.staleDate) || isAfterDate(cachedBannerOptions.staleDate) ) { // Update in the background; still use old list until then getListOfBannersFromApi; }	return $.Deferred.resolve(cachedBanners.value, cachedBannerOptions.value); }; var setBannersAndBannerOptions = function(banners, bannerOptions) { config.banners = banners; config.bannerOptions = bannerOptions; return true; };

config.banners = {}; config.gotListOfBanners = getBannersFromCache.then(	setBannersAndBannerOptions,	getListOfBannersFromApi );

/* ========== Page class ======================================================================== */ // Extended version of mw.Title  /** * @class Page * @constructor * @param {string} title * Title of the page (can be URI encoded) * @throws {Error} When the title is invalid */ 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 = []; }; /** * Page.newFromText * Constructor with a null return instead of an exception for invalid titles. * * @static * @param {string} t * Title of the page * @return {Page|null} A valid Page object or null if the title is invalid */ Page.newFromText = function(t) { if ( mw.Title.newFromText(t) ) { return new Page(t); } else { return null; } }; // -- Page prototype */ // Inherit from mw.Title Page.prototype = Object.create(mw.Title.prototype); Page.prototype.constructor = Page; // Additional functions /** * getTalk * * Get the name of the page's talk page (for subject-space pages) or the page itself (for * talk-space pages) * * @return {string} Talk page name, includng namespace prefix */ 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; }; /** * getSubject * * Get the name of the page's subject page (for talk-space pages) or the page itself (for * subject-space pages) * * @return {string} Subject page name, includng namespace prefix */ 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; }; /** * getListasAutofill * * Get the autofill value for the "listas" parameter ('Last, First Middle+', no disambiguation) * * @return {string} autofill value for "listas" parameter */ Page.prototype.getListasAutofill = function { var name = this.getMainText.replace(/\s\(.*\)/, ''); if ( name.indexOf(' ') === -1 ) { return name; }	var generationalSuffix = ''; if ( / (?:[JS]r.?|[IVX]+)$/.test(name) ) { generationalSuffix = name.slice(name.lastIndexOf(' ')); name = name.slice(0, name.lastIndexOf(' ')); if ( name.indexOf(' ') === -1 ) { return name + generationalSuffix; }	}	var lastName = name.slice(name.lastIndexOf(' ')+1).replace(/,$/, ''); var otherNames = name.slice(0, name.lastIndexOf(' ')); return lastName + ', ' + otherNames + generationalSuffix; }; /** * getRedirectOrPrefixedText * * Get the page name of this page's redirect target (if applicable), or of this page itself * * @return {string} page name, with namespace prefix */	Page.prototype.getRedirectOrPrefixedText = function { return ( this.redirectsTo ) ? this.redirectsTo.getPrefixedText : this.getPrefixedText; }; /** * getRedirectOrMainText * * Get the page name, without the namespace prefix, of this page's redirect target (if applicable), * or of this page itself * * @return {string} page name, without namespace prefix */	Page.prototype.getRedirectOrMainText = function { return ( this.redirectsTo ) ? this.redirectsTo.getMainText : this.getMainText; }; /** * getBannerFromNameOrRedirect * * Get one of this page's banners (Template objects) from either its page name, or from the name of * the page it redirects to. * * @param {string} bannerNameOrRedirect * @return {Template|boolean} Template object if found, or `false` if not found */	Page.prototype.getBannerFromNameOrRedirect = function(bannerNameOrRedirect) { if ( !this.banners ) { return false; }	var toCheckTitleObject = mw.Title.newFromText('Template:'+bannerNameOrRedirect); var toCheckPrefixedText = toCheckTitleObject && toCheckTitleObject.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; }; /** * getTalkpageTopSection * * Retrieve the wikitext of the top section of the talk page, and store it as {this}.oldTopSection * * @return {jQuery.Deferred} Deferred object: resolved once oldTopSection is set, or rejected * if the Api request fails */ 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; }; /** * getLatestSubjectRevisionID * * Retrieve the revision ID of subject page's latest revision, and store it as * {this}.latestSubjectRevisionID * * @return {jQuery.Deferred} Deferred object: resolved once latestSubjectRevisionID is set, or rejected * if the Api request fails */ 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; }; /** * getOresScore * * Retrieve the ORES score of subject page's latest revision, and store it as * {this}.oresScore * * @return {jQuery.Deferred} Deferred object: resolved once oresScore is set, or rejected * if the Api request fails */ 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; }; /** * getSubjectRawWikitext * * Get the raw wikitext of subject page * * @return {jQuery.Deferred} Deferred object: resolves with the raw wikitext, or is rejected with * http error details if the request fails */ Page.prototype.getSubjectRawWikitext = function { return API.getRaw(this.getSubject); }; /** * makeEdit * * Makes the edit to the talk page * * @return {jQuery.Deferred} Deferred object: resolved if the edit is done, or is rejected with * Api error details if the request fails */ 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; }; /** * makePreview * * Make HTML of a preview of the edit * * @return {jQuery.Deferred} Deferred object: resolves with the preview HTML, or is rejected with * Api error details if the request fails */ Page.prototype.makePreview = function { var self = this; var madePreview = $.Deferred; API.get({		action: 'parse',		contentmodel: 'wikitext',		text: self.makeNewTopSection,		title: self.getTalk,		pst: 1	}) .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; }; /** * makeDiff * * Make HTML of a diff to the current wikitext. * * @return {jQuery.Deferred} Deferred object: resolves with the diff HTML, or is rejected with * Api error details if the request fails */ 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');			return;		}		var diffTable = $(' is not a recognised WikiProject banner. Do you want to continue?"					);				} 			}		} );		prompt.opened.then( function {			windowManager.getCurrentWindow.input.focus;		});		prompt.closed.then( function ( data ) {			if ( !data || !data.input ) { return; }			var existingBanner = self.page.getBannerFromNameOrRedirect(data.input);			if ( existingBanner && !existingBanner.remove ) {				alert('There is already a banner!');				return;			}			var templateObject;			if ( existingBanner ) {				existingBanner.remove = false;				existingBanner.parameters = {};				existingBanner.touched = {};				templateObject = existingBanner;			} else {				templateObject = new Template('Template:'+data.input);				templateObject.isProjectBanner = true;				self.page.banners.push(templateObject);			}			var dabWarningDeferred = $.Deferred;			var talkDoesNotExist = $('#ca-talk.new').length !== 0;			var bannerIsWpDab    = templateObject.getTransclusionName === 'WikiProject Disambiguation'; var isOnlyBanner   = self.page.banners.filter(function(b){ return !b.remove; }).length === 1; if ( talkDoesNotExist && bannerIsWpDab && isOnlyBanner ) { OO.ui.confirm(					"New talk pages shouldn't be created if they will only contain the banner. Continue?",					{title: 'Warning'}				) .then(function(confirmed) {					if ( confirmed ) {						dabWarningDeferred.resolve('confirmed');					} else {						dabWarningDeferred.reject('cancelled');						self.page.banners = self.page.banners.filter(function(b){ return b.getTransclusionName !== 'WikiProject Disambiguation'; });					}				});			} else { dabWarningDeferred.resolve('notApplicable'); }			// Retrieve extra data -- but always resolve because we can still work without it			var redirectsToDeferred = $.Deferred; $.when(!!existingBanner || templateObject.setRedirectsTo) .always(function { redirectsToDeferred.resolve; });

var classesAndImportancesDeferred = $.Deferred; $.when(!!existingBanner || templateObject.parseClassesAndImportances) .always(function { classesAndImportancesDeferred.resolve; }); var templateDataDeferred = $.Deferred; $.when(!!existingBanner || templateObject.setParamDataAndSuggestions) .always(function { templateDataDeferred.resolve; });

$.when(				dabWarningDeferred,				redirectsToDeferred,				classesAndImportancesDeferred,				templateDataDeferred			) .then( function {				// set required/suggested parameters to either the autovaule, or				// an empty string (without touching, unless it is a required parameter)				$.each(templateObject.paramData, function(paraName, paraData) { if ( paraData.required || paraData.suggested ) {

templateObject.setParamValue(paraName, paraData.autovalue || '', !paraData.required); }				});				var newRow = self.makeRow(templateObject).insertBefore('#rater-dialog-actions');				self.autofillParams(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($(' ').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($(' ').attr('disabled','disabled').text('Importance')); var setAll = Dialog.makeButton({		label:	'Set all',		icon:	'tag',		accessKey: 'b' //Create access key to go straight to Set all button	}); setAll.find('span.oo-ui-labelElement-label').append(		classForAllDropdown,		importanceForAllDropdown	); var pageInfo = $(' '); if ( self.page.oresScore ) { pageInfo.append(			$(' ').append( Dialog.makeIcon('ores', false), ' ',				extraJs.makeLink('mw:ORES', 'ORES'), ' Predicted class: ', $('').text(self.page.oresScore) )		);	}	if ( self.page.subjectRedirectsTo ) { pageInfo.append(			$(' ').append( Dialog.makeIcon('redirect', false), ' Page redirects to: ', extraJs.makeLink(self.page.subjectRedirectsTo) )		);	}	return $(' ').attr('id', 'rater-dialog-actions').addClass('rater-dialog-row').append(		pageInfo,		$(' ').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'],		accessKey: 's'	}).click(function { self.onSaveClick; }); var preview = Dialog.makeButton({		label:	'Show preview',		accessKey: 'p'	}).click(function {		self.showOverlayDialog( self.page.makePreview, 'Preview', 'changes' );	});

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

var close = Dialog.makeButton({		label:	'Close'	}) .attr('id', 'rater-dialog-button-close') .click(function { self.close; }); self.emptyContent; self.setFooterButtons(null); self.addToBody('Saving...'); self.page.makeEdit .done( function {		self.addToBody('Done!');		self.setFooterButtons(close);		$('#rater-dialog-button-close').find('a').focus;		setTimeout(function { $('#rater-dialog-button-close').click; }, 5000);	})	.fail( function(code, jqxhr) {		self.addToBody('Failed. ' + extraJs.makeErrorMsg(code, jqxhr));		self.setFooterButtons(close);		$('#rater-dialog-button-close').find('a').focus;	} ); };

Dialog.prototype.autofillParams = 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 === '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 && !self.page.subjectRedirectsTo ) { $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'); });			}		});	}	// Autofill listas parameter for WP:BIO var wpbioBanner = self.page.getBannerFromNameOrRedirect('WikiProject Biography'); if ( wpbioBanner && !wpbioBanner.parameters.listas ) { var $wpbioRow = $('div.rater-dialog-row').has('span.rater-dialog-templateName:contains("'+			wpbioBanner.getTransclusionName + '")'); var $listasInput = $wpbioRow.find('span.rater-dialog-paraInput').has('span:contains("listas")') .find('input'); $listasInput.val(self.page.getListasAutofill).blur; $listasInput.parent.addClass('rater-dialog-autofill') .on('keypress.first change.first', function {			$(this).removeClass('rater-dialog-autofill').off('keypress.first change.first');		}); } };

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.autofillParams;					}

self.makeButtons; self.resetHeight; // Focus on Add WikiProject (first button within actions row) $("#rater-dialog-actions").find('span.oo-ui-buttonElement').first.find('a').focus; 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; };

/* ========== Set up current page and dialog ==================================================== */ var setupRater = function(clickEvent) { if ( clickEvent ) { clickEvent.preventDefault; }	var currentPage = Page.newFromText(config.mw.wgPageName); var progressBar = new OO.ui.ProgressBarWidget( {		progress: 1	} ); var incrementProgress = function(amount, maximum) { var priorProgress = progressBar.getProgress; var incrementedProgress = Math.min(maximum || 100, priorProgress + amount); progressBar.setProgress(incrementedProgress); };	var incrementProgressByInterval = function { var incrementIntervalDelay = 100; var incrementIntervalAmount = 0.1; var incrementIntervalMaxval = 98; return window.setInterval(			incrementProgress,			incrementIntervalDelay,			incrementIntervalAmount,			incrementIntervalMaxval		); };	var dialog = new Dialog(currentPage); dialog.addToBody(		$(' ').attr('id', 'dialog-loading').append( progressBar.$element, $(' ').attr('id', 'dialog-loading-0').css('font-weight', 'bold').text('Initialising:'), $(' ').attr('id', 'dialog-loading-1').text('Loading talkpage wikitext...'), $(' ').attr('id', 'dialog-loading-2').text('Parsing talkpage templates...'), $(' ').attr('id', 'dialog-loading-3').text('Checking if page redirects...'), $(' ').attr('id', 'dialog-loading-4').text('Retrieving quality prediction...').hide, $(' ').attr('id', 'dialog-loading-5').text('Building interface...') )	);	var showTaskDone = function(taskNumber) { $('#dialog-loading-'+taskNumber).append(' Done!'); var isLastTask = ( taskNumber === 5 ); if ( isLastTask ) { // Immediately show 100% completed incrementProgress(100); window.setTimeout(function {				$('#dialog-loading').hide;			}, 100); return; } 		// Show a smooth transition by using small steps over a short duration var totalIncrement = 20; var totalTime = 400; var totalSteps = 10; var incrementPerStep = totalIncrement / totalSteps; for ( var step=0; step < totalSteps; step++) { window.setTimeout(				incrementProgress,				totalTime * step / totalSteps,				incrementPerStep			); }	};	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 .then(		function { showTaskDone(1); },		function(code, jqxhr) { showTaskFailed(1, code, jqxhr); }	) .then(function{		return currentPage.setTemplatesBanners		.then( function { showTaskDone(2); }, function(code, jqxhr) { showTaskFailed(2, code, jqxhr); } );	});

// Check if page is a redirect - but don't error out if request fails var redirDeferred = $.Deferred; currentPage.getSubjectRawWikitext .always(function(rawPage) { 		if ( /^\s*#REDIRECT/i.test(rawPage) ) {			currentPage.subjectRedirectsTo = rawPage.slice(rawPage.indexOf()+2, rawPage.indexOf()) || true;		}		showTaskDone(3);		redirDeferred.resolve;	});

// Retrieve rating from ORES var oresDeferred = ( config.mw.wgNamespaceNumber <= 1 ) ? $.Deferred : ''; if ( oresDeferred ) { $('#dialog-loading-4').show; redirDeferred.always(function {			if ( currentPage.subjectRedirectsTo ) {				showTaskDone(4);				oresDeferred.resolve;				return;			}			currentPage.getOresScore			.then( function { showTaskDone(4); oresDeferred.resolve; },				function(code, jqxhr) { showTaskFailed(4, code, jqxhr); setTimeout(oresDeferred.resolve, 2000); }			);		});	}

// Build dialog $.when(oresDeferred, redirDeferred, talkDeferred) .then(function {		var incrementProgressInterval = incrementProgressByInterval;		var dialogBuiltDeferred = dialog.build(true);		dialogBuiltDeferred.always(function { window.clearInterval(incrementProgressInterval); });       return dialogBuiltDeferred;    }) .then(		function{			showTaskDone(5);           clearInvalidCacheItems;		},		function(code, jqxhr) { showTaskFailed(5, code, jqxhr); }	);

};

// Add portlet link mw.util.addPortletLink(	'p-cactions',	'#',	'Rater',	'ca-rater',	'Rate quality and importance',	'a' //changed access key to permit alt+shift access key modifiers in browsers like Vivaldi ); $('#ca-rater').click(setupRater);

// Autostart if no wikiproject banners on talk page (function autoStart {	if ( window.rater_autostartNamespaces == null || config.mw.wgIsMainPage ) {		return;	}	var autostartNamespaces = ( $.isArray(window.rater_autostartNamespaces) ) ? window.rater_autostartNamespaces : [window.rater_autostartNamespaces];	if ( -1 === autostartNamespaces.indexOf(config.mw.wgNamespaceNumber) ) {		return;	}	if ( /(?:\?|&)(?:action|diff|oldid)=/.test(window.location.href) ) {		return;	}	// Check if talk page exists	if ( $('#ca-talk.new').length ) {		setupRater;		return;	}	var thisPage = Page.newFromText(config.mw.wgPageName);

/* Check templates present on talk page. Fetches indirectly transcluded templates, so will find Template:WPBannerMeta (and its subtemplates). But some banners such as MILHIST don't use that meta template, so we also have to check for template titles containg 'WikiProject' */	API.get({		action: 'query',		format: 'json',		prop: 'templates',		titles: thisPage.getTalk,		tlnamespace: '10',		tllimit: '500',		indexpageids: 1	}) .done(function(result) {		var id = result.query.pageids;		var templates = result.query.pages[id].templates;		if ( !templates ) {			setupRater;			return;		}		// Array.prototype.reduce is compatible with more browsers than Array.prototype.includes		var hasWikiproject = templates.reduce(function(resultSoFar, currentTemplate) { return ( resultSoFar || /(WikiProject|WPBanner)/.test(currentTemplate.title) ); }, false);		if ( !hasWikiproject ) {			setupRater;			return;		}	}) .fail(function(code, jqxhr) {		// Silently ignore failures (just log to console)		console.warn( '[Rater] Error while checking whether to autostart.' + ( code == null ) ? '' : ' ' + extraJs.makeErrorMsg(code, jqxhr) );	});

});

/* ========== Export code for testing by /test.js =============================================== */ window.testRater = { 'config': config, 'API': API,/* 'getListOfBanners': getListOfBanners,*/ 'Page': Page, 'Template': Template, 'SuggestionLookupTextInputWidget': SuggestionLookupTextInputWidget, 'ComboBoxInputPrompt': ComboBoxInputPrompt, 'OverlayDialog': OverlayDialog, 'windowFactory': windowFactory, 'windowManager': windowManager, 'Dialog': Dialog, 'setupRater': setupRater, 'writeToCache': writeToCache/*, 'autoStart': autoStart*/ };

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