MediaWiki:Gadget-WmfProjectStatusHelper.js

来自工场百科

注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的变更的影响。

  • Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5Ctrl-R(Mac为⌘-R
  • Google Chrome:Ctrl-Shift-R(Mac为⌘-Shift-R
  • Internet Explorer或Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5
  • Opera:Ctrl-F5
/*
	Project Status Helper
	rmoen@wikimedia.org
	psh.js v1.0

TODO: Conflict handling:
	Edit, Replace, Cleanup
*/

var testTxt = '';
( function( mw, $ ) {

	/* Project status helper class */
	Psh = function( project ) {
		this.project = {
			date: '',
			name: '',
			desc: ''
		};
		if( project !== undefined && project.date !== undefined ) {
			this.project.date = project.date;
		} else {
			this.project.date = ( new Date() ).toJSON().substring( 0, 10 );
		}
		if( project !== undefined && project.name !== undefined ) {
			this.project.name = project.name;
		} else {
			this.project.name = mw.config.get( 'wgTitle' ).replace( '/status', '' );
		}

		var	_this = this,
			todayFormatted = this.project.date.replace( 'monthly', '28' ),
			isMonthly = ( this.project.date.search( 'monthly' ) > -1 ),
			$modalElements =
			$( '<form>' )
				.attr( 'id', 'projectStatusHelperForm' )
				.append(
					$( '<div>' )
						.attr( 'class', 'mw-ajax-loader' )
						.css({
							position: 'absolute',
							width: '100%',
							height: '100%',
							top: '0px',
							left: '0px'
						})
						.hide()
				).append(
					$( '<div>' ).attr( 'id', 'projectStatusHelperInput' )
						.append(
							$( '<div>' )
								.append(
									$( '<label>' )
										.attr( 'for', 'projectStatusName' )
										.text( 'Project Name:' )
								).append(
									$( '<br>' ).css( 'clear', 'both' )
								).append(
									$( '<select>' )
										.attr({
											name: 'projectStatusName',
											id: 'projectStatusName'
										})
										.bind( 'change', function() {
											_this.updateStatusLink();
										}).append(
											$( '<option>' )
												.attr( 'value', '' )
												.text( '- Please Select Project -' )
										)
								)
						).append(
							$( '<div>' )
								.append(
									$( '<span>' )
										.text( 'Project Link: ' )
								).append(
									$( '<a>' )
										.attr({
											id: 'projectLink',
											target: '_blank'
										}).css( 'text-decoration', 'underline' )
								)
						).append(
							$( '<div>' )
								.append(
									$( '<label>' )
										.attr( 'for', 'projectStatusDate' )
										.text( 'Status Date:' )
								).append(
									$( '<br>' ).css({'clear': 'both'})
								).append(
									$( '<input>' )
										.attr({
											type: 'text',
											name: 'projectStatusDate',
											id: 'projectStatusDate'
										})
										.val( todayFormatted )
										.bind( 'change', function() {
											_this.fillDescriptionField();
										})
								).append(
									$( '<small>' ).text( 'eg. YYYY-MM-DD' )
								)
						).append(
							$( '<div>' )
								.append(
									$( '<label>' )
										.attr( 'for', 'projectStatusMonthlyFlag' )
										.text( 'Include in Monthly report?' )
								).append(
									$( '<input>' )
										.attr({
											type: 'checkbox',
											name: 'projectStatusMonthlyFlag',
											id: 'projectStatusMonthlyFlag'
										})
										.prop( 'checked', isMonthly )
										.bind( 'change', function() {
											_this.fillDescriptionField();
										})
								)
						).append(
							$( '<div>' )
								.append(
									$( '<label>' )
										.attr( 'for', 'projectStatusUpdateDescription' )
										.text( 'Description:' )
								).append(
									$( '<textarea>' )
										.attr({
											name: 'projectStatusUpdateDescription',
											id: 'projectStatusUpdateDescription',
											style: 'width:98.5% !important'
										}).css({
											padding: '5px',
											fontSize: '12px',
											height: '130px'
										// TODO: remove this after testing
										}).html( testTxt )
								)
						)
				).append(
					$( '<div>' )
						.attr( 'id', 'projectStatusPreview' )
						.css({
							backgroundColor: '#FDFFE7',
							border: '1px solid #FCEB92',
							padding: '5px',
							height: '310px',
							overflowY: 'scroll',
							marginBottom: '5px'
						}).hide()
				)/*
				.append(
					$('<button>')
						.attr({'id': 'projectStatusBackBtn'})
						.text( 'Back' )
						.click( function( e ) {
							e.preventDefault();
							_this.back();
						}).hide()
				).append(
					$( '<button>' )
						.attr({'id': 'projectStatusPreviewBtn'})
						.text( 'Preview' )
						.click( function( e ) {
							e.preventDefault();
							_this.preview();
						})
				).append(
					$( '<button>' )
						.attr({'id': ''})
						.text( 'Publish' )
						.click( function( e ) {
							e.preventDefault();
							_this.publish();
						})
				)*/;

		this.uneditedDescription = '';

		var projectPages = [
			'Wikimedia_Features_engineering',
			'Wikimedia_Platform_Engineering',
			'Wikimedia_mobile_engineering',
			'Analytics',
			'Wikimedia_Language_engineering'
		];
		//{{Wikimedia project index line|TimedMediaHandler}}
		var lookFor = '{{Wikimedia project index line|';
		// Testing getPages
		this.getPages( projectPages, function( pages ) {
			for ( var page in pages ) {
				_this.scrubPage(
					pages[page],
					lookFor,
					buildProjectsObject
				);
			}
			_this.updateStatusLink();
			_this.fillDescriptionField();
		} );

		function buildProjectsObject( title, names ) {
			var $projectSel = $modalElements.find( '#projectStatusName' );
			var $projectGroup = $( '<optgroup>' ).attr( 'label', title );
			var name;

			for ( var i = 0; i < names.length; i++ ) {
				name = names[i]
					.replace( lookFor, '' )
					.replace( '}}', '' );
				$projectGroup
					.append(
						$('<option>')
							.attr( 'value', name )
							.prop( 'selected', name === _this.project.name )
							.text( name )
				);
			}
			$projectSel.append( $projectGroup );
		}
		$( '#psh-dialog' ).remove();
		// add to the DOM
		$( 'body' ).append(
			$( '<div>' ).attr({
				id: 'psh-dialog',
				title: 'Project Status Helper'
			})
			.append(
				$modalElements
			)
			.hide()
		);

		mw.loader.using( ['jquery.ui.dialog', 'jquery.ui.datepicker'], function() {
			$( '#psh-dialog' ).dialog({
				height: 'auto',
				width: 800,
				modal: true,
				buttons: [
					{
						id: 'projectStatusBackBtn',
						text: 'Back',
						click: function() {
							_this.back();
						}
					},
					{
						id: 'projectStatusPreviewBtn',
						text: 'Preview',
						click: function() {
							_this.preview();
						}
					},
					{
						id: 'projectStatusPublishBtn',
						text: 'Publish',
						click: function() {
							_this.publish();
						}
					}
				]
			});
			// hide preview back button
			$( '#projectStatusBackBtn' ).hide();
			// datepicker that
			$( '#projectStatusDate' )
				.datepicker({'dateFormat': 'yy-mm-dd'});
		});
		this.$modal = $( '#psh-dialog' );
		return this;
	};

	Psh.prototype.scrubPage = function( page, lookFor, callback ) {
		var lines, i;
		var found = [];
		var content = page.revisions[0]['*'];
		var title = page.title;

		if ( content.length > 0 ) {
			lines = content.split( /\n/ );

			// Loop through lines
			for ( i = 0; i < lines.length; i++ ) {
				if ( lines[i].indexOf( lookFor ) !== -1 ) {
					found.push(
						lines[i]
					);
				}
			}
			callback( title, found );
		}
	};

	Psh.prototype.addStatus = function() {
		var _this = this,
			summary = 'new status update';

		// Send the wikitext to the parser for rendering
		$.ajax({
			url: mw.util.wikiScript( 'api' ),
			data: {
				action: 'edit',
				// Project page name...
				title: this.pageName,
				//appendtext: this.wikitext,
				text: this.wikitext,
				token: mw.user.tokens.get( 'editToken' ),
				format: 'json',
				summary: summary,
				notminor: true
			},
			dataType: 'json',
			type: 'POST',
			success: function( data ) {
				if ( data && data.edit && data.edit.result === 'Success' ) {
					_this.hideSpinner();
					window.location.reload( true );
				}
			},
			error: function() {}
		});
	};

	Psh.prototype.getPages = function( titles, callback ) {
		var pagesQuery = $.isArray( titles ) ? titles.join( '|' ) : titles;
		$.ajax({
			url: mw.util.wikiScript( 'api' ),
			data: {
				action: 'query',
				format: 'json',
				titles: pagesQuery,
				prop: 'revisions',
				rvprop: 'content'
			},
			dataType: 'json',
			type: 'GET',
			cache: 'false',
			success: function( data ) {
				if ( data && data.query && data.query.pages ) {
					// return pages to callback
					if( typeof callback === 'function' ) {
						callback( data.query.pages );
					}
				}
			},
			error: function() {
				if( typeof callback === 'function' ) {
					callback( {} );
				}
			}
		});

	};

	/* quick and dirty validation form */
	Psh.prototype.validate = function() {
		for ( var prop in this.project ) {
			if ( this.project[prop] === '' ) {
				return false;
			}
		}
		return true;
	};

	Psh.prototype.fillDescriptionField = function() {
		var _this = this;
		var $descField = _this.$modal.find( '#projectStatusUpdateDescription' );
		var currentdesc = $descField.val();
		if ( this.uneditedDescription != currentdesc && currentdesc != '' ) {
			if ( !confirm( 'Replace existing entry with content from date (if available)?' ) ) {
				return false;
			}
		}

		this.setupProject();
		if ( this.project.name == '' ) {
			_this.pageContent = '';
			return false;
		}
		this.getPages( this.pageName, function( pages ) {
			_this.fillStatusPageContent( pages );
			var splitEntry = _this.getStatusPageSplitAtDate( _this.project.date );
			var desc = _this.stripEntry( splitEntry['middle'] );
			$descField.val( desc );
			_this.uneditedDescription = desc;
		});
	};

	Psh.prototype.stripEntry = function( content ) {
		// strip the title
		content = content.replace( /^== *([^= ]*) *==\n\n?/m, '' );
		// ...and the begin and end tags
		content = content.replace( /^\s*<section[\s]+begin[^>]*\/>\s*\n?/mg, '' );
		content = content.replace( /<section[\s]+end[^>]*\/>\s*\n?$/mg, '' );
		return content;
	};

	Psh.prototype.fillStatusPageContent = function( pages ) {
		// use only first page in pages object.
		for ( var page in pages ) {
			// if page exists the title will be in the returned object
			if ( pages[page].hasOwnProperty( 'revisions' ) ) {
				// save wikitext, if page empty pageContent will be ''
				this.pageContent = pages[page].revisions[0]['*'];
			} else {
				// page does not exist
				this.pageContent = '';
			}
			break;
		}
	};

	/* query for page */
	Psh.prototype.publish = function() {
		var _this = this;

		this.showSpinner();
		this.setupProject();
		//console.log( this.project );
		if ( this.validate() === false ) {
			alert( 'Project name, date, and description are required.' );
			this.hideSpinner();
			return ;
		}
		this.getPages( this.pageName, function( pages ) {
			_this.fillStatusPageContent( pages );
			_this.prepareContentAndUpdate();
		});
	};

	Psh.prototype.setupProject = function() {
		var $helperForm = this.$modal.find( '#projectStatusHelperForm' );

		this.project = {
			date: $helperForm.find( '#projectStatusDate' ).val(),
			name: $helperForm.find( '#projectStatusName' ).val(),
			monthly: $helperForm.find( '#projectStatusMonthlyFlag' ).prop( 'checked' ),
			desc: $helperForm.find( '#projectStatusUpdateDescription' ).val()
		};
		// if monthly status update
		if ( this.project.monthly ) {
			this.project.date = this.project.date.substring( 0, 8 ) + 'monthly';
		}
		this.project.latestUpdate = 'Last update on: <section begin="latest"/>' +
			this.project.date + '<section end="latest"/>';
		this.wikitext = '';
	};

	Psh.prototype.getStatusPageSplitAtDate = function( splitDate ) {
		var statusLines = [],
			lookFor = '';
		// for replacement, gather text before and after entry
		var splitEntry = {
			'firstline': '',
			'before': '',
			'middle': '',
			'after': ''
		};

		if ( this.pageContent.length > 0 ) {
			statusLines = this.pageContent.split( /\n/ );
			splitEntry['firstline'] = statusLines[0] + '\n';

			var datematch = null;
			var splitState = 'before';

			// skip the first line, populate 'before', 'middle', and 'after'
			for ( var i = 1; i < statusLines.length; i++ ) {
				datematch = statusLines[i].match( /^== *([^= ]*) *==$/ );
				if ( datematch != null ) {
					if ( datematch[1] == splitDate ) {
						splitState = 'middle';
					} else if ( splitState == 'middle' ) {
						// we must be starting a new section
						splitState = 'after';
					}

				}
				splitEntry[splitState] += statusLines[i] + '\n';
			}
		}
		return splitEntry;
	};

	/**
	 * replace this.wikitext with new version containing latest update,
	 * then push it to the wiki.
	 */
	Psh.prototype.prepareContentAndUpdate = function() {
		var splitEntry = this.getStatusPageSplitAtDate( this.project.date );
		if( splitEntry['middle'] != '' ) {
			if ( confirm( 'Replace existing entry?' ) ) {
				splitstate = 'middle';
			} else {
				//console.log( 'abort' );
				this.hideSpinner();
				return false;
			}
		}
		this.wikitext = this.project.latestUpdate + '\n';
		this.wikitext += splitEntry['before'];
		if( splitEntry['middle'] == '' ) {
			this.wikitext = this.wikitext.replace( /\n*$/m, '\n\n' );
		}
		this.wikitext += this.buildWikitext();
		this.wikitext += splitEntry['after'];
		this.addStatus();
		return true;
	};

	Psh.prototype.buildWikitext = function() {
		var retval = '';
		retval = '== ' + this.project.date + ' ==\n\n';
		retval += '<section begin="'+ this.project.date + '"/>' +
			this.project.desc + '<section end="' + this.project.date + '"/>';
		retval += '\n\n';
		return retval;
	};

	Psh.prototype.preview = function() {
		var _this = this;

		$( '#projectStatusPreviewBtn' ).hide();
		$( '#projectStatusBackBtn' ).show();

		this.showSpinner();
		this.setupProject();
		wikitext = this.buildWikitext();

		// Send the wikitext to the parser for rendering
		$.ajax({
			url: mw.util.wikiScript( 'api' ),
			data: {
				'action': 'parse',
				'title': this.pageName,
				'format': 'json',
				'text': wikitext,
				'prop': 'text',
				'pst': true
			},
			dataType: 'json',
			type: 'POST',
			success: function ( data ) {
				_this.$modal.find( '#projectStatusPreview' )
					.html( data.parse.text['*'] )
					.show()
					.prev()
					.hide();
				_this.$modal.find( '.editsection' ).remove();
				_this.hideSpinner();
			},
			error: function() {}
		});
	};

	Psh.prototype.back = function() {
		$( '#projectStatusHelperInput, #projectStatusPreviewBtn' ).show();
		$( '#projectStatusPreview, #projectStatusBackBtn' ).hide();
	};

	Psh.prototype.showSpinner = function() {
		this.$modal
			.find( '.mw-ajax-loader' )
			.show();
	};

	Psh.prototype.hideSpinner = function() {
		this.$modal
			.find( '.mw-ajax-loader' )
			.hide();
	};

	Psh.prototype.updateStatusLink = function() {
		var path = mw.config.get( 'wgServer' ) + mw.config.get( 'wgArticlePath' ),
		$inputElement =
			this.$modal
			.find( '#projectStatusName' );
		var selectedProject = $inputElement.val();

		if( selectedProject != '' ) {
			this.pageName = $inputElement.val() + '/status';
			this.pageURL = path.replace( '$1', this.pageName );

			$( '#projectLink' )
				.attr( 'href', this.pageURL )
				.text( this.pageName );
		}
	};

	$( document ).ready( function() {
		// Add a link to the toolbox
		var link = mw.util.addPortletLink(
			'p-tb',
			'#',
			'Project Status',
			't-prettylinkwidget',
			'Dialog to help you submit your project updates',
			null,
			'#t-projectstatushelper'
		);
		// Setup link click event
		$( link ).click( function( e ) {
			e.preventDefault();
			var psh = new Psh();
			$( '#projectStatusHelperInput div' ).css({
				'margin-top': '10px'
			});
		});

		// override edit links on project pages
		$( '.mw-statushelper-editlink > a' ).each( function( i ) {
			$( this ).click( function( event ) {
				event.preventDefault();
				var projectObj = {};
				projectObj.name = $( '.mw-statushelper-editlink' )[i].getAttribute( 'data-statuspage' ).replace( '/status', '' );
				projectObj.date = $( '.mw-statushelper-editlink' )[i].getAttribute( 'data-entrydate' );
				var psh = new Psh( projectObj );
				$( '#projectStatusHelperInput div' ).css({
					'margin-top': '10px'
				});
			});
		});

		// override add links on project pages
		$( '.mw-statushelper-addlink > a' ).each( function( i ) {
			$( this ).click( function( event ) {
				event.preventDefault();
				var projectObj = {};
				projectObj.name = $( '.mw-statushelper-addlink' )[i].getAttribute( 'data-statuspage' ).replace( '/status', '' );
				var psh = new Psh( projectObj );
				$( '#projectStatusHelperInput div' ).css({
					'margin-top': '10px'
				});
			});
		});
	});
})( mediaWiki, jQuery );