User:McIntireEvan/afch-rewrite.js/submissions.js

/* Uploaded from https://github.com/WPAFC/afch-rewrite, commit: 7fd970065baa1bb02e9c1d0511512a5f27c664f0 (master) */ // ( function ( AFCH, $, mw ) {	var $afchLaunchLink, $afch, $afchWrapper,		afchPage, afchSubmission, afchViews, afchViewer;

// Die if reviewing a nonexistent page or a userjs/css page if ( mw.config.get( 'wgArticleId' ) === 0 ||		mw.config.get( 'wgPageContentModel' ) !== 'wikitext' ) {		return; }

/**	 * Represents an AfC submission -- its status as well as comments. * Call submission.parse to actually run the parsing process and fill * the object with useful data. *	 * @param {AFCH.Page} page The submission page */	AFCH.Submission = function ( page ) { // The associated page this.page = page;

// 'WT:Articles for creation/Foo' => 'Foo' this.shortTitle = this.page.title.getMainText.match( /([^\/]+)\/?$/ )[1];

this.resetVariables; };

/**	 * Resets variables and lists related to the submission state */	AFCH.Submission.prototype.resetVariables = function { // Various submission states, set in parse this.isPending = false; this.isUnderReview = false; this.isDeclined = false; this.isDraft = false;

// Set in updateAttributesAfterParse this.isCurrentlySubmitted = false; this.hasAfcTemplate = false;

// All parameters on the page, zipped up into one // pretty package. The most recent value for any given // parameter (based on `ts`) takes precedent. this.params = {};

// Holds all of the templates that still // apply to the page this.templates = [];

// Holds all comments on the page this.comments = [];

// Holds all submitters currently displayed on the page // (indicated by the `u` parameter) this.submitters = []; };

/**	 * Parses a submission, writing its current status and data to various properties * @return {$.Deferred} Resolves with the submission when parsed successfully */	AFCH.Submission.prototype.parse = function { var sub = this, deferred = $.Deferred;

this.page.getTemplates.done( function ( templates ) {			sub.loadDataFromTemplates( templates );			sub.sortAndParseInternalData;			deferred.resolve( sub );		} );

return deferred; };

/**	 * Internal function * @param {array} templates list of templates to parse */	AFCH.Submission.prototype.loadDataFromTemplates = function ( templates ) { // Represent each AfC submission template as an object. var submissionTemplates = [], commentTemplates = [];

$.each( templates, function ( _, template ) {			var name = template.target.toLowerCase;			if ( name === 'afc submission' ) {				submissionTemplates.push( { status: ( AFCH.getAndDelete( template.params, '1' ) || '' ).toLowerCase, timestamp: AFCH.getAndDelete( template.params, 'ts' ) || '', params: template.params } );			} else if ( name === 'afc comment' ) {				commentTemplates.push( { // If we can't find a timestamp, set it to unicorns, because everyone // knows that unicorns always come first. timestamp: AFCH.parseForTimestamp( template.params['1'], /* mwstyle */ true ) || 'unicorns', text: template.params['1'] } );			}		} );

this.templates = submissionTemplates; this.comments = commentTemplates; };

/**	 * Sort the internal lists of AFC submission and Afc comment templates */	AFCH.Submission.prototype.sortAndParseInternalData = function { var sub = this, submissionTemplates = this.templates, commentTemplates = this.comments;

function timestampSortHelper ( a, b ) { // If we're passed something that's not a number -- // for example, -- just sort it // first and be done with it. if ( isNaN( a.timestamp ) ) { return -1; } else if ( isNaN( b.timestamp ) ) { return 1; }

// Otherwise just sort normally return +b.timestamp - +a.timestamp; }

// Sort templates by timestamp; most recent are first submissionTemplates.sort( timestampSortHelper ); commentTemplates.sort( timestampSortHelper );

// Reset variables related to the submisson state before re-parsing this.resetVariables;

// Useful list of "what to do" in each situation. var statusCases = { // Declined d: function { if ( !sub.isPending && !sub.isDraft && !sub.isUnderReview ) { sub.isDeclined = true; }				return true; },			// Draft t: function { // If it's been submitted or declined, remove draft tag if ( sub.isPending || sub.isDeclined || sub.isUnderReview ) { return false; }				sub.isDraft = true; return true; },			// Under review r: function { if ( !sub.isPending && !sub.isDeclined ) { sub.isUnderReview = true; }				return true; },			// Pending '': function { // Remove duplicate pending templates or a redundant // pending template when the submission has already been // declined / is already under review if ( sub.isPending || sub.isDeclined || sub.isUnderReview ) { return false; }				sub.isPending = true; sub.isDraft = false; sub.isUnderReview = false; return true; }		};

// Process the submission templates in order, from the most recent to // the oldest. In the process, we remove unneeded templates (for example,		// a draft tag when it's already been submitted) and also set various // "isX" properties of the Submission. submissionTemplates = $.grep( submissionTemplates, function ( template ) {			var keepTemplate = true;

if ( statusCases[template.status] ) { keepTemplate = statusCases[template.status]; } else { // Default pending status keepTemplate = statusCases['']; }

// If we're going to be keeping this template on the page, // save the parameter and submitter data. When saving params, // don't overwrite parameters that are already set, because // we're going newest to oldest (i.e. save most recent only). if ( keepTemplate ) { // Save parameter data sub.params = $.extend( {}, template.params, sub.params );

// Save submitter if not already listed if ( sub.submitters.indexOf( template.params.u ) === -1 ) { sub.submitters.push( template.params.u ); }

// Will be re-added in makeWikicode if necessary delete template.params.small; // small=yes for old declines }

return keepTemplate; } );

this.isCurrentlySubmitted = this.isPending || this.isUnderReview; this.hasAfcTemplate = !!submissionTemplates.length;

this.templates = submissionTemplates; this.comments = commentTemplates; };

/**	 * Converts all the data to a hunk of wikicode * @return {string} */	AFCH.Submission.prototype.makeWikicode = function { var output = [], hasDeclineTemplate = false;

// Submission templates go first $.each( this.templates, function ( _, template ) {			var tout = ' ';

output.push( tout ); } );

// Then comment templates $.each( this.comments, function ( _, comment ) {			output.push( '\n' );		} );

// If there were comments, add a horizontal rule beneath them if ( this.comments.length ) { output.push( '\n' ); }

return output.join( '\n' ); };

/**	 * Checks if submission is G13 eligible * @return {$.Deferred} Resolves to bool if submission is eligible */	AFCH.Submission.prototype.isG13Eligible = function { var deferred = $.Deferred;

// Submission must not currently be submitted if ( this.isCurrentlySubmitted ) { return deferred.resolve( false ); }

// Userspace drafts and `Draft` namespace drafts must have // one or more AFC submission templates to be eligible if ( [ 2, 118 ].indexOf( this.page.title.getNamespaceId ) !== -1 &&			this.templates.length === 0 ) {			return deferred.resolve( false ); }

// And not have been modified in 6 months // FIXME: Ignore bot edits? this.page.getLastModifiedDate.done( function ( lastEdited ) {			var timeNow = new Date,				sixMonthsAgo = new Date;

sixMonthsAgo.setMonth( timeNow.getMonth - 6 );

deferred.resolve( ( timeNow.getTime - lastEdited.getTime ) >				( timeNow.getTime - sixMonthsAgo.getTime ) ); } );

return deferred; };

/**	 * Sets the submission status * @param {string} newStatus status to set, 'd'|'t'|'r'|'' * @param {params} optional; params to add to the template whose status was set * @return {bool} success */	AFCH.Submission.prototype.setStatus = function ( newStatus, newParams ) { var relevantTemplate = this.templates[0];

if ( [ 'd', 't', 'r', '' ].indexOf( newStatus ) === -1 ) { // Unrecognized status return false; }

if ( !newParams ) { newParams = {}; }

// If there are no templates on the page, just generate a new one // (addNewTemplate handles the reparsing) if ( !relevantTemplate ||			// Same for if the top template on the stack is alrady declined;			// we don't want to overwrite it			relevantTemplate.status === 'd' ) {			this.addNewTemplate( {					status: newStatus,					params: newParams			} ); } else { // Just modify the template at the top of the stack relevantTemplate.status = newStatus; relevantTemplate.params.ns = mw.config.get( 'wgNamespaceNumber' );

// Add new parameters if specified $.extend( relevantTemplate.params, newParams );

// And finally reparse this.sortAndParseInternalData; }

return true; };

/**	 * Add a new template to the beginning of this.templates * @param {object} data object with properties of template *                     - status (default: '') *                     - timestamp (default: '') *                     - params (default: {}) */	AFCH.Submission.prototype.addNewTemplate = function ( data ) { this.templates.unshift( $.extend( /* deep */ true, { status: '', timestamp: '', params: { ns: mw.config.get( 'wgNamespaceNumber' ) }		}, data ) );

// Reparse :P this.sortAndParseInternalData; };

/**	 * Add a new comment to the beginning of this.comments * @param {string} text comment text * @return {bool} success */	AFCH.Submission.prototype.addNewComment = function ( text ) { var commentText = $.trim( text );

if ( commentText.indexOf( '~' ) === -1 ) { commentText += ' ~'; }

this.comments.unshift( {			// Unicorns are explained in loadDataFromTemplates			timestamp: AFCH.parseForTimestamp( commentText, /* mwstyle */ true ) || 'unicorns',			text: commentText		} );

// Reparse :P this.sortAndParseInternalData;

return true; };

/**	 * Gets the submitter, or, if no specific submitter is available, * just the page creator *	 * @return {$.Deferred} resolves with user */	AFCH.Submission.prototype.getSubmitter = function { var deferred = $.Deferred, user = this.params.u;

if ( user ) { deferred.resolve( user ); } else { this.page.getCreator.done( function ( user ) {				deferred.resolve( user );			} ); }

return deferred; };

/**	 * Gets the next submission in the queue (i.e. the oldest one). * @param {bool} older Whether to get the oldest pending submission instead of the newest one. * @return {$.Deferred} resolves with API call */	AFCH.Submission.prototype.getNextSubmission = function ( older ) { var deferred = $.Deferred, request = { action: 'query', list: 'categorymembers', cmtitle: 'Category:Pending AfC submissions', cmnamespace: 5, cmtype: 'page', cmlimit: 1, cmsort: 'sortkey', cmdir: older ? 'desc' : 'asc', cmstartsortkey: 'P' + ( ( this.templates.length && this.templates[0].timestamp ) / 100 ) + ( older ? 0 : 1 ) };

AFCH.api.get( request ) .done( function ( data ) {				if ( data.query.categorymembers && data.query.categorymembers.length && data.query.categorymembers[0].title ) {					deferred.resolve( data.query.categorymembers[0].title );				} else {					deferred.reject( data );				}			} );

return deferred; };

/**	 * Represents text of an AfC submission * @param {[type]} text [description] */	AFCH.Text = function ( text ) { this.text = text; };

AFCH.Text.prototype.get = function { return this.text; };

AFCH.Text.prototype.set = function ( string ) { this.text = string; return this.text; };

AFCH.Text.prototype.prepend = function ( string ) { this.text = string + this.text; return this.text; };

AFCH.Text.prototype.append = function ( string ) { this.text += string; return this.text; };

AFCH.Text.prototype.cleanUp = function ( isAccept ) { var text = this.text, commentRegex, commentsToRemove = [ 'Please don\'t change anything and press save', 'Carry on from here, and delete this comment.', 'Please leave this line alone!', 'Important, do not remove this line before (template|article) has been created.', 'Just press the "Save page" button below without changing anything! Doing so will submit your article submission for review. ' +					'Once you have saved this page you will find a new yellow \'Review waiting\' box at the bottom of your submission page. ' +					'If you have submitted your page previously,(?: either)? the old pink \'Submission declined\' template or the old grey ' + '\'Draft\' template will still appear at the top of your submission page, but you should ignore (them|it). Again, please ' + 'don\'t change anything in this text box. Just press the \"Save page\" button below.', ];

if ( isAccept ) { // Uncomment cats and templates text = text.replace( /\[\[:Category:/gi, '[[Category:' );			text = text.replace( /\{\{(tl|tlx|tlg)\|(.*?)\}\}/ig, '');

// Strip the AFC G13 postponement template text = text.replace( /\{\{AfC postpone G13(?:\|\d*)?\}\}\n*/gi, '' );

// Add to the list of comments to remove $.merge( commentsToRemove, [				'Enter template purpose and instructions here.',				'Enter the content and\\/or code of the template here.',				'EDIT BELOW THIS LINE',				'After listing your sources please cite them using inline citations and place them after the information they cite. ' +					'Please see (https?://en.wikipedia.org/wiki/Wikipedia:REFB|\\[\\[Wikipedia:REFB\\]\\]) for instructions on how to add citations.',			] ); } else { // If not yet accepted, comment out cats text = text.replace( /\[\[Category:/gi, '[[:Category:' );		}

// Assemble a master regexp and remove all now-unneeded comments (commentsToRemove) commentRegex = new RegExp( '<!-{2,}\\s*(' + commentsToRemove.join( '|' ) + ')\\s*-{2,}>', 'gi' ); text = text.replace( commentRegex, '' );

// Remove initial request artifact text = text.replace( /== Request review at \[\[WP:AFC\]\] ==/gi, '' );

// Remove sandbox templates text = text.replace( /\{\{(userspacedraft|userspace draft|user sandbox|Please leave this line alone \(sandbox heading\))(?:\{\{[^{}]*\}\}|[^}{])*\}\}/ig, '' );

// Remove html comments (/gi, '$1');

// Remove spaces/commas between tags text = text.replace( /\s*(<\/\s*ref\s*\>)\s*[,]*\s*(<\s*ref\s*(name\s*=|group\s*=)*\s*[^\/]*>)[ \t]*$/gim, '$1$2' );

// Remove whitespace before tags text = text.replace( /[ \t]*(<\s*ref\s*(name\s*=|group\s*=)*\s*.*[^\/]+>)[ \t]*$/gim, '$1' );

// Move punctuation before tags text = text.replace( /\s*((<\s*ref\s*(name\s*=|group\s*=)*\s*.*[\/]{1}>)|(<\s*ref\s*(name\s*=|group\s*=)*\s*[^\/]*>(?:<[^<\>]*\>|[^><])*<\/\s*ref\s*\>))[ \t]*([.!?,;:])+$/gim, '$6$1' );

// Replace with "* http://example.com/foo" (common newbie error) text = text.replace( /\n\{\{(http[s]?|ftp[s]?|irc|gopher|telnet)\:\/\/(.*?)\}\}/gi, '\n* $1://$3' );

// Convert http://-style links to other wikipages to wikicode syntax // FIXME: Break this out into its own core function? Will it be used elsewhere? function convertExternalLinksToWikilinks ( text ) { var linkRegex = /\[{1,2}(?:https?:)?\/\/(?:en.wikipedia.org\/wiki|enwp.org)\/([^\s\|\]\[]+)(?:\s|\|)?((?:\[\^\[\*\]\]|[^\]\[])*)\]{1,2}/ig, linkMatch = linkRegex.exec( text ), title, displayTitle, newLink;

while ( linkMatch ) { title = decodeURI( linkMatch[1] ).replace( /_/g, ' ' ); displayTitle = decodeURI( linkMatch[2] ).replace( /_/g, ' ' );

// Don't include the displayTitle if it is equal to the title if ( displayTitle && title !== displayTitle ) { newLink =  + displayTitle + ; } else { newLink =  + title + ; }

text = text.replace( linkMatch[0], newLink ); linkMatch = linkRegex.exec( text ); }

return text; }

text = convertExternalLinksToWikilinks( text );

this.text = text; this.removeExcessNewlines;

return this.text; };

AFCH.Text.prototype.removeExcessNewlines = function { // Replace 3+ newlines with just two this.text = this.text.replace( /(?:[\t ]*(?:\r?\n|\r)){3,}/ig, '\n\n' ); // Remove all whitespace at the top of the article this.text = this.text.replace( /^\s*/, '' ); };

AFCH.Text.prototype.removeAfcTemplates = function { // FIXME: Awful regex to remove the old submission templates // This is bad. It works for most cases but has a hellish time // with some double nested templates or faux nested templates (for		// example "{{hi|{ foo}}" -- note the extra bracket). Ideally Parsoid // would just return the raw template text as well (currently		// working on a patch for that, actually). this.text = this.text.replace( new RegExp( '\\{\\{\\s*afc submission\\s*(?:\\||[^ – ]*|)*?\\}\\}' + // Also remove the AFCH-generated warning message, since if necessary the script will add it again '?', 'gi' ), '' ); this.text = this.text.replace( /\{\{\s*afc comment\s*(?:\||[^ – ]*|)*?\}\}/gi, '' );

// Remove horizontal rules that were added by AFCH after the comments this.text = this.text.replace( /^+$/gm, '' );

// Remove excess newlines created by AFC templates this.removeExcessNewlines;

return this.text; };

/**	 * Removes old submission templates/comments and then adds new ones * specified by `new` * @param {string} new */	AFCH.Text.prototype.updateAfcTemplates = function ( newCode ) { this.removeAfcTemplates; return this.prepend( newCode + '\n\n' ); };

AFCH.Text.prototype.updateCategories = function ( categories ) { var catIndex, match, text = this.text, categoryRegex = /\[\[:?Category:.*?\s*\]\]/gi, newCategoryCode = '\n';

// Create the wikicode block $.each( categories, function ( _, category ) {			var trimmed = $.trim( category );			if ( trimmed ) {				newCategoryCode += '\n';			}		} );

match = categoryRegex.exec( text );

// If there are no categories currently on the page, // just add the categories at the bottom if ( !match ) { text += newCategoryCode; // If there are categories on the page, remove them all, and // then add the new categories where the last category used to be		} else { while ( match ) { catIndex = match.index; text = text.replace( match[0], '' ); match = categoryRegex.exec( text ); }

text = text.substring( 0, catIndex ) + newCategoryCode + text.substring( catIndex ); }

this.text = text; return this.text; };

// Add the launch link $afchLaunchLink = $( mw.util.addPortletLink( AFCH.prefs.launchLinkPosition, '#', 'Review (AFCH)', 'afch-launch', 'Review submission using afch-rewrite', '1' ) );

if ( AFCH.prefs.autoOpen &&		// Don't autoload in userspace -- too many false positives		AFCH.consts.pagename.indexOf( 'User:' ) !== 0 &&		// Only autoload if viewing or editing the page		[ 'view', 'edit', null ].indexOf( AFCH.getParam( 'action' ) ) !== -1 &&		!AFCH.getParam( 'diff' ) && !AFCH.getParam( 'oldid' ) ) {		// Launch the script immediately if preference set createAFCHInstance; } else { // Otherwise, wait for a click (`one` to prevent spawning multiple panels) $afchLaunchLink.one( 'click', createAFCHInstance ); }

// Mark launch link for the old helper script as "old" if present on page $( '#p-cactions #ca-afcHelper > a' ).append( ' (old)' );

// If AFCH is destroyed via AFCH.destroy, // remove the $afch window and the launch link AFCH.addDestroyFunction( function {		$afchLaunchLink.remove;

// The $afch window might not exist yet; make // sure it does before trying to remove it :)		if ( $afch && $afch.jquery ) {			$afch.remove;		}	} );

function createAFCHInstance { /**		 * global; wraps ALL afch-y things */		$afch = $( ' ' ) .addClass( 'afch' ) .insertBefore( '#mw-content-text' ) .append(				$( ' ' )					.addClass( 'top-bar' )					.append( // Back link appears on the left based on context $( ' ' )							.addClass( 'back-link' ) .html( '&#x25c0; back to options' ) // back arrow .attr( 'title', 'Go back' ) .addClass( 'hidden' ) .click( function {								// Reload the review panel								spinnerAndRun( setupReviewPanel );							} ),

// On the right, a close button $( ' ' )							.addClass( 'close-link' ) .html( '&times;' ) .click( function {								// DIE DIE DIE (...then allow clicks on the launch link again)								$afch.remove;								$afchLaunchLink									.off( 'click' ) // Get rid of old handler									.one( 'click', createAFCHInstance );							} ) )			);

/**		 * global; wrapper for specific afch panels */		$afchWrapper = $( ' ' ) .addClass( 'panel-wrapper' ) .appendTo( $afch ) .append(				// Build splash panel in JavaScript rather than via				// a template so we don't have to wait for the				// HTTP request to go through				$( ' ' )						.addClass( 'review-panel' )						.addClass( 'splash' )						.append( $( ' ' )								.addClass( 'initial-header' ) .text( 'Loading AFCH v' + AFCH.consts.version + '...' ) )				);

// Now set up the review panel and replace it with actual content, not just a splash screen setupReviewPanel;

// If the "Review" link is clicked again, just reload the main view $afchLaunchLink.click( function {			spinnerAndRun( setupReviewPanel );		} ); }

function setupReviewPanel { // Store this to a variable so we can wait for its success var loadViews = $.ajax( {				type: 'GET',				url: AFCH.consts.baseurl + '/tpl-submissions.js',				dataType: 'text'			} ).done( function ( data ) {				/* global */				afchViews = new AFCH.Views( data );				/* global */				afchViewer = new AFCH.Viewer( afchViews, $afchWrapper );			} );

/* global */ afchPage = new AFCH.Page( AFCH.consts.pagename );

/* global */ afchSubmission = new AFCH.Submission( afchPage );

// Set up messages for later setMessages;

// Parse the page and load the view templates. When done, // continue with everything else. $.when(			afchSubmission.parse,			loadViews		).then( function ( submission ) {			var extrasRevealed = false;

// Render the base buttons view loadView( 'main', {				title: submission.shortTitle,				accept: submission.isCurrentlySubmitted,				decline: submission.isCurrentlySubmitted,				comment: true, // Comments are always okay!				submit: !submission.isCurrentlySubmitted,				alreadyUnderReview: submission.isUnderReview,				version: AFCH.consts.version,				versionName: AFCH.consts.versionName			} );

// Set up the extra options slide-out panel, which appears // when the user click on the chevron $afch.find( '#afchExtra .open' ).click( function {				var $extra = $afch.find( '#afchExtra' ),					$toggle = $( this );

if ( extrasRevealed ) { $extra.find( 'a' ).hide; $extra.stop.animate( { width: '20px' }, 100, 'swing', function {						extrasRevealed = false;					} ); } else { $extra.stop.animate( { width: '210px' }, 150, 'swing', function {						$extra.find( 'a' ).css( 'display', 'block' );						extrasRevealed = true;					} ); }			} );

// Add feedback and preferences links AFCH.initFeedback( $afch.find( 'span.feedback-wrapper' ), '[your topic here]', 'give feedback' ); AFCH.preferences.initLink( $afch.find( 'span.preferences-wrapper' ), 'preferences' );

// Set up click handlers $afch.find( '#afchAccept' ).click( function { spinnerAndRun( showAcceptOptions ); } ); $afch.find( '#afchDecline' ).click( function { spinnerAndRun( showDeclineOptions ); } ); $afch.find( '#afchComment' ).click( function { spinnerAndRun( showCommentOptions ); } ); $afch.find( '#afchSubmit' ).click( function { spinnerAndRun( showSubmitOptions ); } ); $afch.find( '#afchClean' ).click( function { handleCleanup; } ); $afch.find( '#afchMark' ).click( function { handleMark( /* unmark */ submission.isUnderReview ); } );

// Load warnings about the page, then slide them in			getSubmissionWarnings.done( function ( warnings ) {				if ( warnings.length ) {					// FIXME: CSS-based slide-in animation instead to avoid having					// to use stupid hide + removeClass workaround?					$afch.find( '.warnings' )						.append( warnings )						.hide.removeClass( 'hidden' )						.slideDown;				}			} );

// Get G13 eligibility and when known, display relevant buttons... // but don't hold up the rest of the loading to do so			submission.isG13Eligible.done( function ( eligible ) {				$afch.find( '.g13-related' ).toggleClass( 'hidden', !eligible );				$afch.find( '#afchG13' ).click( function { handleG13; } );				$afch.find( '#afchPostponeG13' ).click( function  { spinnerAndRun( showPostponeG13Options ); } );			} ); } );	}

/**	 * Loads warnings about the submission * @return {jQuery} */	function getSubmissionWarnings { var deferred = $.Deferred, warnings = [];

/**		 * Adds a warning * @param {string} message * @param {string|bool} actionMessage set to false to hide action link * @param {function|string} onAction function to call of success, or URL to browse to		 */ function addWarning ( message, actionMessage, onAction ) { var $action, $warning = $( ' ' ) .addClass( 'afch-warning' ) .text( message );

if ( actionMessage !== false ) { $action = $( '' ) .addClass( 'link' ) .text( actionMessage || 'Edit page' ) .appendTo( $warning );

if ( typeof onAction === 'function' ) { $action.click( onAction ); } else { $action .attr( 'target', '_blank' ) .attr( 'href', onAction || mw.util.getUrl( AFCH.consts.pagename, { action: 'edit' } ) ); }			}

warnings.push( $warning ); }

function checkReferences { var deferred = $.Deferred;

afchPage.getText( true ).done( function ( text ) {				var refBeginRe = /<\s*ref.*?\s*>/ig,					refBeginMatches = $.grep( text.match( refBeginRe ) || [], function ( ref ) { // If the ref is closed already, we don't want it						// (returning true keeps the item, false removes it) return ref.indexOf( '/>', ref.length - 2 ) === -1; } ),

refEndRe = /<\/\s*ref\s*\>/ig, refEndMatches = text.match( refEndRe )|| [],

reflistRe = /|(<\s*references\s*\/?>)/ig, hasReflist = reflistRe.test( text ),

// This isn't as good as a tokenizer, and believes that tags if ( refBeginMatches.length !== refEndMatches.length ) { addWarning( 'The submission contains ' +						( refBeginMatches.length > refEndMatches.length ? 'unclosed' : 'unbalanced' ) + ' detection				if ( malformedRefs.length ) {					addWarning( 'The submission contains malformed tags.', 'View details', function { var $toggleLink = $( this ).addClass( 'malformed-refs-toggle' ), $warningDiv = $( this ).parent; $malformedRefWrapper = $( ' ' ) .addClass( 'malformed-refs' ) .appendTo( $warningDiv );

// Show the relevant code snippets $.each( malformedRefs, function ( _, ref ) {							$( ' ' )								.addClass( 'code-wrapper' )								.append( $( ' ' ).text( ref ) )								.appendTo( $malformedRefWrapper );						} );

// Now change the "View details" link to behave as a normal toggle for .malformed-refs AFCH.makeToggle( '.malformed-refs-toggle', '.malformed-refs', 'View details', 'Hide details' );

return false; } );				}

// after if ( hasReflist ) { if ( refBeginRe.test( text.substring( reflistRe.lastIndex ) ) ) { addWarning( 'Not all of the tags are before the references list. You may not see all references.' ); }				}

// without if ( refBeginMatches.length && !hasReflist ) { addWarning( 'The submission contains tags, but has no references list! You may not see all references.' ); }

deferred.resolve; } );

return deferred; }

function checkDeletionLog { var deferred = $.Deferred;

// Don't show deletion notices for "sandbox" to avoid useless // information when reviewing user sandboxes and the like if ( afchSubmission.shortTitle.toLowerCase === 'sandbox' ) { deferred.resolve; return deferred; }

AFCH.api.get( {				action: 'query',				list: 'logevents',				leprop: 'user|timestamp|comment',				leaction: 'delete/delete',				letype: 'delete',				lelimit: 10,				letitle: afchSubmission.shortTitle			} ).done( function ( data ) {				var rawDeletions = data.query.logevents;

if ( !rawDeletions.length ) { deferred.resolve; return; }

addWarning( 'The page "' + afchSubmission.shortTitle + '" has been deleted ' + rawDeletions.length + ( rawDeletions.length === 10 ? '+' : '' ) +					' time' + ( rawDeletions.length > 1 ? 's' : '' ) + '.', 'View deletion log', function {						var $toggleLink = $( this ).addClass( 'deletion-log-toggle' ),							$warningDiv = $toggleLink.parent,							deletions = [];

$.each( rawDeletions, function ( _, deletion ) {							deletions.push( { timestamp: deletion.timestamp, relativeTimestamp: AFCH.relativeTimeSince( deletion.timestamp ), deletor: deletion.user, deletorLink: mw.util.getUrl( 'User:' + deletion.user ), reason: AFCH.convertWikilinksToHTML( deletion.comment ) } );						} );

$( afchViews.renderView( 'warning-deletions-table', { deletions: deletions } ) ) .addClass( 'deletion-log' ) .appendTo( $warningDiv );

// ...and now convert the link into a toggle which simply hides/shows the div AFCH.makeToggle( '.deletion-log-toggle', '.deletion-log', 'View deletion log', 'Hide deletion log' );

return false; } );

deferred.resolve;

} );

return deferred; }

function checkReviewState { var reviewer;

if ( afchSubmission.isUnderReview ) { // Use a more personal "You" if the user is the reviewer if ( afchSubmission.params.reviewer === AFCH.consts.user ) { reviewer = 'You'; } else { reviewer = afchSubmission.params.reviewer || 'Someone'; }

addWarning( reviewer + ( afchSubmission.params.reviewts ? ' began reviewing this submission ' + AFCH.relativeTimeSince( afchSubmission.params.reviewts ) : ' already began reviewing this submission' ) + '.',					'View page history', mw.util.getUrl( AFCH.consts.pagename, { action: 'history' } ) ); }		}

function checkLongComments { var deferred = $.Deferred;

afchPage.getText( true ).done( function ( rawText ) {				var					// Simulate cleanUp first so that we don't warn about HTML					// comments that the script will remove anyway in the future					text = ( new AFCH.Text( rawText ) ).cleanUp( true ),					longCommentRegex = /(?:<![ \r\n\t]*--)([^\-]|[\r\n]|-[^\-]){30,}(?:--[ \r\n\t]*>)?/g,					longCommentMatches = text.match( longCommentRegex ) || [],					numberOfComments = longCommentMatches.length,					oneComment = numberOfComments === 1;

if ( numberOfComments ) { addWarning( 'The page contains ' + ( oneComment ? 'an' : '' ) + ' HTML comment' + ( oneComment ? '' : 's' ) +						' longer than 30 characters.', 'View comment' + ( oneComment ? '' : 's' ), function {							var $toggleLink = $( this ).addClass( 'long-comment-toggle' ),								$warningDiv = $( this ).parent,								$commentsWrapper = $( ' ' )									.addClass( 'long-comments' )									.appendTo( $warningDiv );

// Show the relevant code snippets $.each( longCommentMatches, function ( _, comment ) {								$( ' ' )									.addClass( 'code-wrapper' )									.append( $( ' ' ).text( $.trim( comment ) ) )									.appendTo( $commentsWrapper );							} );

// Now change the "View comment" link to behave as a normal toggle for .long-comments AFCH.makeToggle( '.long-comment-toggle', '.long-comments',								'View comment' + ( oneComment ? '' : 's' ), 'Hide comment' + ( oneComment ? '' : 's' ) );

return false; } );				}

deferred.resolve; } );

return deferred; }

$.when(			checkReferences,			checkDeletionLog,			checkReviewState,			checkLongComments		).then( function {			deferred.resolve( warnings );		} );

return deferred; }

/**	 * Stores useful strings to AFCH.msg */	function setMessages { AFCH.msg.set( {			// $1 = article name			// $2 = article class or '' if not available			'accepted-submission': '== Your submission at Articles for creation: ' +				'$1 has been accepted ==\n$1',

// $1 = full submission title // $2 = short title // $3 = copyright violation ('yes'/'no') // $4 = decline reason code // $5 = decline reason additional parameter 'declined-submission': '== Your submission at Articles for creation: ' + '$2 ==\n',

// $1 = article name 'comment-on-submission': 'comment',

// $1 = article name 'g13-submission': '$1 ~',

'teahouse-invite': '' } );	}

/**	 * Clear the viewer, set up the status log, and * then update the button text * @param {string} actionTitle optional, if there is no content available and the *                            script has to load a new view, this will be its title * @param {string} actionClass optional, if there is no content available and the *                            script has to load a new view, this will be the class *                            applied to it	 */ function prepareForProcessing ( actionTitle, actionClass ) { var $content = $afch.find( '#afchContent' ), $submitBtn = $content.find( '#afchSubmitForm' );

// If we can't find a submit button or a content area, load // a new temporary "processing" stage instead if ( !( $submitBtn.length || $content.length ) ) { loadView( 'quick-action-processing', {				actionTitle: actionTitle || 'Processing',				actionClass: actionClass || 'other-action'			} );

// Now update the variables $content = $afch.find( '#afchContent' ); $submitBtn = $content.find( '#afchSubmitForm' ); }

// Empty the content area except for the button... $content.contents.not( $submitBtn ).remove;

// ...and set up the status log in its place AFCH.status.init( '#afchContent' );

// Update the button show the `running` text $submitBtn .text( $submitBtn.data( 'running' ) ) .addClass( 'disabled' ) .off( 'click' );

// Handler will run after the main AJAX requests complete setupAjaxStopHandler;

}

/**	 * Sets up the `ajaxStop` handler which runs after all ajax * requests are complete and changes the text of the button * to "Done", shows a link to the next submission and * auto-reloads the page. */	function setupAjaxStopHandler { $( document ).ajaxStop( function {			$afch.find( '#afchSubmitForm' )				.text( 'Done' )				.append( ' ',					$( '' ) .attr( 'id', 'reloadLink' ) .addClass( 'text-smaller' ) .attr( 'href', mw.util.getUrl ) .text( '(reloading...)' ) );

afchSubmission.getNextSubmission.done( function ( title ) {				new AFCH.status.Element( 'Continue to next submission, $1 &raquo;', { '$1': AFCH.makeLinkElementToPage( title ) } );			} );

// Also, automagically reload the page in place $( '#mw-content-text' ).load( AFCH.consts.pagelink + ' #mw-content-text', function {				$afch.find( '#reloadLink' ).text( '(reloaded automatically)' );				// Fire the hook for new page content				mw.hook( 'wikipage.content' ).fire( $( '#mw-content-text' ) );			} );

// Stop listening to ajaxStop events; otherwise these can stack up if			// the user goes back to perform another action, for example $( document ).off( 'ajaxStop' ); } );	}

/**	 * Adds handler for when the accept/decline/etc form is submitted * that calls a given function and passes an object to the function * containing data from all .afch-input elements in the dom. *	 * Also sets up the viewer for the "processing" stage. *	 * @param {Function} fn function to call with data */	function addFormSubmitHandler ( fn ) { $afch.find( '#afchSubmitForm' ).click( function {			var data = {};

// Provide page text; use cache created after afchSubmission.parse afchPage.getText( true ).done( function ( text ) {				data.afchText = new AFCH.Text( text );

// Also provide the values for each afch-input element $.extend( data, AFCH.getFormValues( $afch.find( '.afch-input' ) ) );

prepareForProcessing;

// Now finally call the applicable handler fn( data ); } );		} );	}

/**	 * Displays a spinner in the main content area and then * calls the passed function * @param {function} fn function to call when spinner has been displayed * @return {[type]} [description] */	function spinnerAndRun ( fn ) { var $spinner, $container = $afch.find( '#afchContent' );

// Add a new spinner if one doesn't already exist if ( !$container.find( '.mw-spinner' ).length ) {

$spinner = $.createSpinner( {				size: 'large',				type: 'block'			} ) // Set the spinner's dimensions equal to the viewers's dimensions so that // the current scroll position is not lost when emptied .css( {					height: $container.height,					width: $container.width				} );

$container.empty.append( $spinner ); }

if ( typeof fn === 'function' ) { fn; }	}

/**	 * Loads a new view * @param {string} name view to be loaded * @param {object} data data to populate the view with * @param {function} callback function to call when view is loaded */	function loadView ( name, data, callback ) { // Show the back button if we're not loading the main view $afch.find( '.back-link' ).toggleClass( 'hidden', name === 'main' ); afchViewer.loadView( name, data ); if ( callback ) { callback; }	}

// These functions show the options before doing something // to a submission.

function showAcceptOptions { /**		 * If possible, use the session storage to get the WikiProject list. * If it hasn't been cached already, load it manually and then cache */		function loadWikiProjectList { var deferred = $.Deferred, wikiProjects = [], // This is so a new version of AFCH will invalidate the WikiProject cache lsKey = 'afch-' + AFCH.consts.version + '-wikiprojects';

if ( window.localStorage && window.localStorage[lsKey] ) { wikiProjects = JSON.parse( window.localStorage[lsKey] ); deferred.resolve( wikiProjects ); } else { $.ajax( {					url: mw.config.get( 'wgServer' ) + '/w/index.php?title=User:Theo%27s_Little_Bot/afchwikiproject.js&action=raw&ctype=text/javascript',					dataType: 'json'				} ).done( function ( projectData ) {					$.each( projectData, function ( display, template ) { wikiProjects.push( {							displayName: display,							templateName: template						} ); } );

// If possible, cache the WikiProject data! if ( window.localStorage ) { try { window.localStorage[lsKey] = JSON.stringify( wikiProjects ); } catch ( e ) { AFCH.log( 'Unable to cache WikiProject list: ' + e.message ); }					}

deferred.resolve( wikiProjects ); } );			}

return deferred; }

$.when( afchPage.getText( true ), loadWikiProjectList ).then( function ( pageText, wikiProjects ) {			loadView( 'accept', { newTitle: afchSubmission.shortTitle, hasWikiProjects: !!wikiProjects.length, wikiProjects: wikiProjects, categories: afchPage.getCategories( /* useApi */ false, /* includeCategoryLinks */ true ), // Only offer to patrol the page if not already patrolled (in other words, if				// the "Mark as patrolled" link can be found in the DOM) showPatrolOption: !!$afch.find( '.patrollink' ).length }, function { $afch.find( '#newAssessment' ).chosen( {					allow_single_deselect: true,					disable_search: true,					width: '140px',					placeholder_text_single: 'Click to select'				} );

$afch.find( '#newWikiProjects' ).chosen( {					placeholder_text_multiple: 'Start typing to filter WikiProjects...',					no_results_text: 'Whoops, no WikiProjects matched in database!',					width: '350px'				} );

// Extend the chosen menu for new WikiProjects. We hackily show a				// "Click to manually add " link -- sadly, jquery.chosen // doesn't support this natively. $afch.find( '#newWikiProjects_chzn input' ).keyup( function ( e ) {					var $chzn = $afch.find( '#newWikiProjects_chzn' ),						$input = $( this ),						newProject = $input.val,						$noResults = $chzn.find( 'li.no-results' );

// Only show "Add " link if there are no results if ( $noResults.length ) { $( ' ' )							.appendTo( $noResults.empty ) .text( 'Whoops, no WikiProjects matched in database! ' ) .append(								$( '' )									.text( 'Click to manually add to the page\'s WikiProject list.' )									.click( function  { var $wikiprojects = $afch.find( '#newWikiProjects' );

$( ' ' )											.attr( 'value', newProject ) .attr( 'selected', true ) .text( newProject ) .appendTo( $wikiprojects );

$wikiprojects.trigger( 'liszt:updated' ); $input.val( '' ); } )							);					}				} );

$afch.find( '#newCategories' ).chosen( {					placeholder_text_multiple: 'Start typing to add categories...',					width: '350px'				} );

// Offer dynamic category suggestions! // Since jquery.chosen doesn't natively support dynamic results, // we sneakily inject some dynamic suggestions instead. Consider // switching to something like Select2 to avoid this hackery... $afch.find( '#newCategories_chzn input' ).keyup( function ( e ) {					var $input = $( this ),						prefix = $input.val,						$categories = $afch.find( '#newCategories' );

// Ignore up/down keys to allow users to navigate through the suggestions, // and don't show results when an empty string is provided if ( [ 38, 40 ].indexOf( e.which ) !== -1 || !prefix ) { return; }

// First we remove leftovers from previous suggestions $categories.children.not( ':selected' ).remove; $categories.trigger( 'liszt:updated' ); $input.val( prefix );

AFCH.api.getCategoriesByPrefix( prefix ).done( function ( categories ) {						var currentCategories = [];

// If the input has changed since we started searching, // don't show outdated results if ( $input.val !== prefix ) { return; }

// Make a list of all of the current categories $categories.find( 'option' ).each( function {							currentCategories.push( this.value );						} );

$.each( categories, function ( _, category ) {							// If the category has already been added, don't offer it as an option							if ( currentCategories.indexOf( category ) !== -1 ) {								return;							}

$( ' ' )								.attr( 'value', category ) .text( category ) .appendTo( $categories ); } );

// Make chosen update suggestions $categories.trigger( 'liszt:updated' ); $input.val( prefix );

// If we couldn't find any matching categories, apologize if ( !categories.length ) { $( '' ) .text( 'No matching categories found.' ) .addClass( 'no-results' ) .appendTo( 'ul.chzn-results' ); }

} );				} );

// Show bio options if Biography option checked $afch.find( '#isBiography' ).change( function {					$afch.find( '#bioOptionsWrapper' ).toggleClass( 'hidden', !this.checked );				} );

function prefillBiographyDetails { var titleParts;

// Prefill `LastName, FirstName` for Biography if the page title is two words and // therefore probably safe to asssume in a `FirstName LastName` format. titleParts = afchSubmission.shortTitle.split( ' ' ); if ( titleParts.length === 2 ) { $afch.find( '#subjectName' ).val( titleParts[1] + ', ' + titleParts[0] ); }				}				prefillBiographyDetails;

// Ask for the month/day IF the birth year has been entered $afch.find( '#birthYear' ).keyup( function {					$afch.find( '#birthMonthDayWrapper' ).toggleClass( 'hidden', !this.value.length );				} );

// Ask for the month/day IF the death year has been entered $afch.find( '#deathYear' ).keyup( function {					$afch.find( '#deathMonthDayWrapper' ).toggleClass( 'hidden', !this.value.length );				} );

// If subject is dead, show options for death details $afch.find( '#lifeStatus' ).change( function {					$afch.find( '#deathWrapper' ).toggleClass( 'hidden', $( this ).val !== 'dead' );				} );

// Show an error if the page title already exists in the mainspace, // or if the title is create-protected and user is not an admin $afch.find( '#newTitle' ).keyup( function {					var page,						$field =  $( this ),						$status = $afch.find( '#titleStatus' ),						$submitButton = $afch.find( '#afchSubmitForm' ),						value = $field.val;

// Reset to a pure state $field.removeClass( 'bad-input' ); $status.text( '' ); $submitButton .removeClass( 'disabled' ) .text( 'Accept & publish' );

// If there is no value, die now, because otherwise mw.Title // will throw an exception due to an invalid title if ( !value ) { return; }					page = new AFCH.Page( value );

var linkToPage = AFCH.jQueryToHtml( AFCH.makeLinkElementToPage( page.rawTitle ) );

AFCH.api.get( {						action: 'query',						prop: 'info',						inprop: 'protection',						titles: 'Talk:' + page.rawTitle					} ).done( function ( data ) {						if ( !data.query.pages.hasOwnProperty( '-1' ) ) {							$status.html( 'The talk page for "' + linkToPage + '" exists.' );						}					} );

$.when(						AFCH.api.isBlacklisted( page ),						AFCH.api.get( { action: 'query', prop: 'info', inprop: 'protection', titles: page.rawTitle } )					).then( function ( isBlacklisted, rawData ) {						var errorHtml, buttonText,							data = rawData[0]; // Get just the result, not the Promise object

// If the page already exists, display an error if ( !data.query.pages.hasOwnProperty( '-1' ) ) { errorHtml = 'Whoops, the page "' + linkToPage + '" already exists.'; buttonText = 'The proposed title already exists'; } else { // If the page doesn't exist but IS create-protected and the // current reviewer is not an admin, also display an error // FIXME: offer one-click request unprotection? $.each( data.query.pages['-1'].protection, function ( _, entry ) {								if ( entry.type === 'create' && entry.level === 'sysop' && $.inArray( 'sysop', mw.config.get( 'wgUserGroups' ) ) === -1 )								{									errorHtml = 'Darn it, "' + linkToPage + '" is create-protected. You will need to request unprotection before accepting.';									buttonText = 'The proposed title is create-protected';								}							} ); }

// Now check the blacklist result, but if another error already exists, // don't bother showing this one too if ( !errorHtml && isBlacklisted !== false ) { errorHtml = 'Shoot! ' + isBlacklisted.reason.replace( /\s+/g, ' ' ); buttonText = 'The proposed title is blacklisted'; }

if ( !errorHtml ) { return; }

// Add a red border around the input field $field.addClass( 'bad-input' ); // Show the error message $status.html( errorHtml );

// Disable the submit button and show an error in its place $submitButton .addClass( 'disabled' ) .text( buttonText ); return; } );				} );

// Update titleStatus $afch.find( '#newTitle' ).trigger( 'keyup' ); } );			addFormSubmitHandler( handleAccept );		} ); }

function showDeclineOptions { loadView( 'decline', {}, function {			var $reasons, $commonSection, declineCounts,				pristineState = $afch.find( '#declineInputWrapper' ).html;

function updateTextfield ( newPrompt, newPlaceholder ) { var wrapper = $afch.find( '#textfieldWrapper' );

// Update label and placeholder wrapper.find( 'label' ).text( newPrompt ); wrapper.find( 'input' ).attr( 'placeholder', newPlaceholder );

// And finally show the textfield wrapper.removeClass( 'hidden' ); }

// Copy most-used options to top of decline dropdown

declineCounts = AFCH.userData.get( 'decline-counts', false );

if ( declineCounts ) { declineList = $.map( declineCounts, function ( _, key ) { return key; } );

// Sort list in descending order (most-used at beginning) declineList.sort( function ( a, b ) {					return declineCounts[b] - declineCounts[a];				} );

$reasons = $afch.find( '#declineReason' ); $commonSection = $( ' ' ) .attr( 'label', 'Frequently used' ) .insertBefore( $reasons.find( 'optgroup' ).first );

// Show the 5 most used options $.each( declineList.splice( 0, 5 ), function ( _, rationale ) {					var $relevant = $reasons.find( 'option[value="' + rationale + '"]' );					$relevant.clone( true ).appendTo( $commonSection );				} ); }

// Set up jquery.chosen for the decline reason $afch.find( '#declineReason' ).chosen( {				placeholder_text_single: 'Select a decline reason...',				no_results_text: 'Whoops, no reasons matched your search. Type "custom" to add a custom rationale instead.',				search_contains: true,				inherit_select_classes: true			} );

// And now add the handlers for when a specific decline reason is selected $afch.find( '#declineReason' ).change( function {				var reason = $afch.find( '#declineReason' ).val,					declineHandlers = {						cv: function  {							var $textfieldWrapper, $addAnotherLink, $clone;

updateTextfield( 'Original URL', 'https://example.com/cake' ); $textfieldWrapper = $afch.find( '#textfieldWrapper' );

$clone = $textfieldWrapper.clone( true );

$addAnotherLink = $( ' ' ) .text( '(add another)' ) .addClass( 'afch-label link' ) .appendTo( $textfieldWrapper ) .hide .click( function {									// Remove the old "add another" link									$( this ).remove;

$clone .find( 'input' ) .attr( 'id', 'copyvioUrl2' ) .end .find( 'label' ) .attr( 'for', 'copyvioUrl2' );

$clone.insertAfter( $textfieldWrapper ); } );

// On keyup show the "add another" link $textfieldWrapper.find( 'input' ).one( 'keyup', function {								$addAnotherLink.fadeIn;							} );

$afch.find( '#blankWrapper' ).add( '#csdWrapper' ) .removeClass( 'hidden' ) .children( 'input' ).prop( 'checked', true ); },

dup: function { updateTextfield( 'Title of duplicate submission (no namespace)', 'Articles for creation/Fudge' ); },

mergeto: function { updateTextfield( 'Article which submission should be merged into', 'Milkshake' ); },

lang: function { updateTextfield( 'Language of the submission if known', 'German' ); },

exists: function { updateTextfield( 'Title of existing article', 'Chocolate chip cookie' ); },

plot: function { updateTextfield( 'Title of existing related article, if one exists', 'Charlie and the Chocolate Factory' ); },

van: function { $afch.find( '#blankWrapper' ).add( '#csdWrapper' ) .removeClass( 'hidden' ) .children( 'input' ).prop( 'checked', true ); },

blp: function { $afch.find( '#blankWrapper' ) .removeClass( 'hidden' ) .children( 'input' ).prop( 'checked', true ); },

// Custom decline rationale reason: function { $afch.find( '#declineTextarea' ) .attr( 'placeholder', 'Enter your decline reason here using wikicode syntax.' ); }					};

// Reset to a pristine state :)				$afch.find( '#declineInputWrapper' ).html( pristineState );

// If there are special options to be displayed for this // particular decline reason, load them now if ( declineHandlers[reason] ) { declineHandlers[reason]; }

$afch.find( '#blankSubmission' ).change( function {					// If blank is not selected, then deselect CSD as well					if ( !this.checked ) {						$afch.find( '#csdSubmission' ).prop( 'checked', false );					}					// ... and just outright hide it					$afch.find( '#csdWrapper' ).toggleClass( 'hidden', !this.checked );				} );

// If a reason has been specified, show the textarea, notify // option, and the submit form button $afch.find( '#declineTextarea' ).add( '#notifyWrapper' ).add( '#afchSubmitForm' ) .toggleClass( 'hidden', !reason ); } );		} );

addFormSubmitHandler( handleDecline ); }

function showCommentOptions { loadView( 'comment', {} ); addFormSubmitHandler( handleComment ); }

function showSubmitOptions { var customSubmitters = [];

// Iterate over the submitters and add them to the custom submitters list, // displayed in the "submit as" dropdown. $.each( afchSubmission.submitters, function ( index, submitter ) {			customSubmitters.push( { name: submitter, description: submitter + ( index === 0 ? ' (most recent submitter)' : ' (past submitter)' ), selected: index === 0 } );		} );

loadView( 'submit', {			customSubmitters: customSubmitters		}, function {			// Show the other textbox when `other` is selected in the menu			$afch.find( '#submitType' ).change( function  { var otherSelected = $afch.find( '#submitType' ).val === 'other'; $afch.find( '#submitterName' ).toggleClass( 'hidden', !otherSelected ); } );		} );

addFormSubmitHandler( handleSubmit ); }

function showPostponeG13Options { loadView( 'postpone-g13', {} ); addFormSubmitHandler( handlePostponeG13 ); }

// These functions actually perform a given action using data passed // in the `data` parameter.

function handleAccept ( data ) { var newText = data.afchText;

AFCH.actions.movePage( afchPage.rawTitle, data.newTitle,			'Publishing accepted Articles for creation submission',			{ movetalk: true } ) // Also move associated talk page if exists (e.g. `Draft_talk:`) .done( function ( moveData ) {				var $patrolLink,					newPage = new AFCH.Page( moveData.to ),					talkPage = newPage.getTalkPage,					recentPage = new AFCH.Page( 'Wikipedia:Articles for creation/recent' ),					talkText = '';

// ARTICLE // ---

newText.removeAfcTemplates;

newText.updateCategories( data.newCategories );

// Clean the page newText.cleanUp( /* isAccept */ true );

// Add biography details if ( data.isBiography ) {

// Persondata newText.append(						'\n'					);

//, which generates DEFAULTSORT as well as					// adds the appropriate birth/death year categories newText.append( '\n'' ) + ''					);

}

newPage.edit( {					contents: newText.get,					summary: 'Cleaning up accepted Articles for creation submission'				} );

// Patrol the new page if desired if ( data.patrolPage ) { $patrolLink = $afch.find( '.patrollink' ); if ( $patrolLink.length ) { AFCH.actions.patrolRcid(							mw.util.getParamValue( 'rcid', $patrolLink.find( 'a' ).attr( 'href' ) ),							newPage.rawTitle // Include the title for a prettier log message						); }				}

// TALK PAGE // -

// Add the AFC banner talkText += '';

// Add biography banner if specified if ( data.isBiography ) { // Ensure we don't have duplicate biography tags AFCH.removeFromArray( data.newWikiProjects, 'WikiProject Biography' );

talkText += ( '\n' ); }

if ( data.newAssessment === 'disambig' &&					$.inArray( 'WikiProject Disambiguation', data.newWikiProjects ) === -1 ) {					data.newWikiProjects.push( 'WikiProject Disambiguation' ); }

// Add WikiProjects $.each( data.newWikiProjects, function ( i, project ) {					talkText += '\n';				} );

talkPage.edit( {					// We prepend the text so that talk page content is not removed					// (e.g. pages in `Draft:` namespace with discussion)					mode: 'prependtext',					contents: talkText + '\n\n',					summary: 'Placing Articles for creation banners'				} );

// NOTIFY SUBMITTER //

if ( data.notifyUser ) { afchSubmission.getSubmitter.done( function ( submitter ) {						AFCH.actions.notifyUser( submitter, { message: AFCH.msg.get( 'accepted-submission',								{ '$1': newPage, '$2': data.newAssessment } ), summary: 'Notification: Your Articles for creation submission has been accepted' } );					} );				}

// AFC/RECENT // --

$.when( recentPage.getText, afchSubmission.getSubmitter ) .then( function ( text, submitter ) {						var newRecentText = text,							matches = text.match( /\s*/gi ),							newTemplate = '\n';

// Remove the older entries (at bottom of the page) if necessary // to ensure we keep only 10 entries at any given point in time while ( matches.length >= 10 ) { newRecentText = newRecentText.replace( matches.pop, '' ); }

newRecentText = newTemplate + newRecentText;

recentPage.edit( {							contents: newRecentText,							summary: 'Adding ' + newPage + ' to list of recent AfC creations'						} ); } );			} );	}

function handleDecline ( data ) { var declineCounts, text = data.afchText, declineReason = data.declineReason, newParams = { '2': declineReason, decliner: AFCH.consts.user, declinets: '' };

// Update decline counts declineCounts = AFCH.userData.get( 'decline-counts', {} );

if ( declineCounts[declineReason] ) { declineCounts[declineReason] += 1; } else { declineCounts[declineReason] = 1; }

AFCH.userData.set( 'decline-counts', declineCounts );

// If this is a custom decline, we include the declineTextarea in the template if ( declineReason === 'reason' ) { newParams['3'] = data.declineTextarea; // But otherwise if addtional text has been entered we just add it as a new comment } else if ( data.declineTextarea ) { afchSubmission.addNewComment( data.declineTextarea ); }

// If a user has entered something in the declineTextfield (for example, a URL or an		// associated page), pass that as the third parameter if ( data.declineTextfield ) { newParams['3'] = data.declineTextfield; }

// Handle submission blanking (csd as well if necessary...except for copyvios, handled later) if ( data.blankSubmission ) { text.set( '' ); }

// Copyright violations get 'd as well if ( declineReason === 'cv' && data.csdSubmission ) { text.prepend( '\n' ); // Include copyvio urls in the decline template as well newParams['3'] = data.declineTextfield + ( data.copyvioUrl2 ? ', ' + data.copyvioUrl2 : '' ); }

// Now update the submission status afchSubmission.setStatus( 'd', newParams );

text.updateAfcTemplates( afchSubmission.makeWikicode ); text.cleanUp;

afchPage.edit( {			contents: text.get,			// For the edit summary, we either grab the full summary text for the decline reason or,			// if it is a custom decline, just the full decline text instead.			summary: 'Declining submission: ' + ( declineReason !== 'reason' ? data.declineReasonTexts[0] : data.declineTextarea )		} );

if ( data.notifyUser ) { afchSubmission.getSubmitter.done( function ( submitter ) {				var userTalk = new AFCH.Page( ( new mw.Title( submitter, 3 ) ).getPrefixedText ),					shouldTeahouse = data.inviteToTeahouse ? $.Deferred : false;

// Check categories on the page to ensure that if the user has already been // invited to the Teahouse, we don't invite them again. if ( data.inviteToTeahouse ) { userTalk.getCategories( /* useApi */ true ).done( function ( categories ) {						var hasTeahouseCat = false,							teahouseCategories = [								'Category:Wikipedians who have received a Teahouse invitation',								'Category:Wikipedians who have received a Teahouse invitation through AfC'							];

$.each( categories, function ( _, cat ) {							if ( teahouseCategories.indexOf( cat ) !== -1 ) {								hasTeahouseCat = true;								return false;							}						} );

shouldTeahouse.resolve( !hasTeahouseCat ); } );				}

$.when( shouldTeahouse ).then( function ( teahouse ) {					var message = AFCH.msg.get( 'declined-submission', { '$1': AFCH.consts.pagename, '$2': afchSubmission.shortTitle, '$3': declineReason === 'cv' ? 'yes' : 'no', '$4': declineReason, '$5': newParams['3'] || '' } );

if ( teahouse ) { message += '\n\n' + AFCH.msg.get( 'teahouse-invite' ); }

AFCH.actions.notifyUser( submitter, {						message: message,						summary: 'Notification: Your Articles for Creation submission has been declined'					} ); } );			} );		}

// Log CSD if necessary if ( data.csdSubmission ) { // FIXME: Only get submitter if needed...? afchSubmission.getSubmitter.done( function ( submitter ) {				AFCH.actions.logCSD( { title: afchPage.rawTitle, reason: declineReason === 'cv' ? 'WP:G12 (db-copyvio)' : 'db-reason (Articles for creation)', usersNotified: data.notifyUser ? [ submitter ] : [] } );			} );		}	}

function handleComment ( data ) { var text = data.afchText;

afchSubmission.addNewComment( data.commentText ); text.updateAfcTemplates( afchSubmission.makeWikicode );

text.cleanUp;

afchPage.edit( {			contents: text.get,			summary: 'Commenting on submission'		} );

if ( data.notifyUser ) { afchSubmission.getSubmitter.done( function ( submitter ) {				AFCH.actions.notifyUser( submitter, { message: AFCH.msg.get( 'comment-on-submission',						{ '$1': AFCH.consts.pagename } ), summary: 'Notification: I\'ve commented on your Articles for Creation submission' } );			} );		}	}

function handleSubmit ( data ) { var text = data.afchText, submitter = $.Deferred, submitType = data.submitType;

if ( submitType === 'other' ) { submitter.resolve( data.submitterName ); } else if ( submitType === 'self' ) { submitter.resolve( AFCH.consts.user ); } else if ( submitType === 'creator' ) { afchPage.getCreator.done( function ( user ) {				submitter.resolve( user );			} ); } else { // Custom selected submitter submitter.resolve( data.submitType ); }

submitter.done( function ( submitter ) {			afchSubmission.setStatus( '', { u: submitter } );

text.updateAfcTemplates( afchSubmission.makeWikicode ); text.cleanUp;

afchPage.edit( {				contents: text.get,				summary: 'Submitting'			} );

} );

}

function handleCleanup { prepareForProcessing( 'Cleaning' );

afchPage.getText( true ).done( function ( rawText ) {			var text = new AFCH.Text( rawText );

// Even though we didn't modify them, still update the templates, // because the order may have changed/been corrected text.updateAfcTemplates( afchSubmission.makeWikicode );

text.cleanUp;

afchPage.edit( {				contents: text.get,				summary: 'Cleaning up submission'			} ); } );	}

function handleMark ( unmark ) { var actionText = ( unmark ? 'Unmarking' : 'Marking' );

prepareForProcessing( actionText, 'mark' );

afchPage.getText( true ).done( function ( rawText ) {			var text = new AFCH.Text( rawText );

if ( unmark ) { afchSubmission.setStatus( '', { reviewer: false, reviewts: false } ); } else { afchSubmission.setStatus( 'r', {					reviewer: AFCH.consts.user,					reviewts: ''				} ); }

text.updateAfcTemplates( afchSubmission.makeWikicode ); text.cleanUp;

afchPage.edit( {				contents: text.get,				summary: actionText + ' submission as under review'			} ); } );	}

function handleG13 { // We start getting the creator now (for notification later) because ajax is		// radical and handles simultaneous requests, but we don't let it delay tagging var gotCreator = afchPage.getCreator;

// Update the display prepareForProcessing( 'Requesting', 'g13' );

// Get the page text and the last modified date (cached!) and tag the page $.when(			afchPage.getText( true ),			afchPage.getLastModifiedDate		).then( function ( rawText, lastModified ) {			var text = new AFCH.Text( rawText );

// Add the deletion tag and clean up for good measure text.prepend( '\n' ); text.cleanUp;

afchPage.edit( {				contents: text.get,				summary: 'Tagging abandoned Articles for creation draft ' +					'for speedy deletion under G13'			} );

// Now notify the page creator as well as any and all previous submitters $.when( gotCreator ).then( function ( creator ) {				var usersToNotify = [ creator ];

$.each( afchSubmission.submitters, function ( _, submitter ) {					// Don't notify the same user multiple times					if ( usersToNotify.indexOf( submitter ) === -1 ) {						usersToNotify.push( submitter );					}				} );

$.each( usersToNotify, function ( _, user ) {					AFCH.actions.notifyUser( user, { message: AFCH.msg.get( 'g13-submission',							{ '$1': AFCH.consts.pagename } ), summary: 'Notification: G13 speedy deletion nomination of ' + AFCH.consts.pagename + '' } );				} );

// And finally log the CSD nomination once all users have been notified AFCH.actions.logCSD( {					title: afchPage.rawTitle,					reason: 'WP:G13 (db-afc)',					usersNotified: usersToNotify				} ); } );		} );	}

function handlePostponeG13 ( data ) { var postponeCode, text = data.afchText, rawText = text.get, postponeRegex = /\{\{AfC postpone G13\s*(?:\|\s*(\d*)\s*)?\}\}/ig; match = postponeRegex.exec( rawText );

// First add the postpone template if ( match ) { if ( match[1] !== undefined ) { postponeCode = ''; } else { postponeCode = ''; }			rawText = rawText.replace( match[0], postponeCode ); } else { rawText += '\n'; }

text.set( rawText );

// Then add the comment if entered if ( data.commentText ) { afchSubmission.addNewComment( data.commentText ); text.updateAfcTemplates( afchSubmission.makeWikicode ); }

text.cleanUp;

afchPage.edit( {			contents: text.get,			summary: 'Postponing G13 speedy deletion'		} ); }

}( AFCH, jQuery, mediaWiki ) ); //