Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*  ______________________________________________________________________________________
 * |                                                                                     |
 * |                    === WARNING: GADGET FILE ===                                     |
 * |                  Changes to this page affect many users.                            |
 * | Please discuss changes on the talk page or on [[MediaWiki_talk:Gadgets-definition]] |
 * |	 before editing.                                                                 |
 * |_____________________________________________________________________________________|
 *
 * "Endorse & Join" feature, to be used by the Wikimedia Foundation's Grants Programme
 */
//<nowiki>

//The stylesheet with all the styles for the endorse & the join gadget
mw.loader.load( '//meta.wikimedia.org/w/index.php?title=MediaWiki:Gadget-addMe.css&action=raw&ctype=text/css', 'text/css' );
/*
 * Common utilities for both the endorse & the join gadget
 */
var gadgetUtilities = function (){
	//A reference to the object
	var that = this;
	//The mw wrapper to access the API
	var api = new mw.Api();
	
	/*
	 * The interface messages or strings are maintained in interfaceMessagesPath & config values eg, 
	 * section-header, the section where the comments are added etc are maintained in configPath
	 */
	this.interfaceMessagesPath = 'MediaWiki:Gadget-addMe/InterfaceText';
	this.configPath = 'MediaWiki:Gadget-addMe/Config';
	
	//The time taken for the page to scroll to the feedback speech bubble (milliseconds)
	this.feedbackScrollTime = 2000;
	
	//The time taken for the feedback speech bubble to disappear (milliseconds)
	this.feedbackDisappearDelay = 10000;
	/*
	 * This function is used to set a cookie to show the speech bubble
	 * on page reload
	 */
	this.setFeedbackCookie = function(value){
		$.cookie(value,true);
	};
	/*
	 * This function is used to check if a has been set by the above function 
	 * to show the speech bubble on page reload
	 */
	this.checkFeedbackCookie = function(value){
		if($.cookie(value)){
			$.cookie(value,null);
			return true;
		}
		else{
			return false;
		}
	};
	/*
	 * To display an error message when an error occurs
	 * in the gadget 
	 */
	this.showErrorMessage =  function(gadget,type){
		var errorAttr = '[localize=error-'+type+']';
		var gadgetID = '.'+gadget;
		$(gadgetID + ' ' + errorAttr).show();
	};
	/*
	 * To remove the error message displayed by the above function 
	 */
	this.removeErrorMessage = function(gadget){
		var gadgetID = '.'+gadget;
		$(gadgetID + ' [localize^="error-"]').hide();
	};
	/*
	 * To detect the type of grant. IEG,PEG etc
	 */
	this.grantType = function(config){
		var grant = mw.config.get('wgTitle').split('/')[0].replace(/ /g,'_');
		if (grant in config){
			return config[grant];
		}
		else{
			return config['default'];
		}
	};
	/*
	 * To detect the users default language
	 */
	this.userLanguage = function(){
		return mw.config.get('wgUserLanguage');
	};
	/*
	 * To detect the language of the page
	 */
	this.contentLanguage = function(){
		return mw.config.get('wgContentLanguage');
	};
	/*
	 * To remove extra spaces & cleanup the comment string
	 */
	this.cleanupText = function(text){
			text = $.trim(text)+' ';
			var indexOf = text.indexOf('~~~~');
			if ( indexOf == -1 ){
				return text;
			}
			else{
				return text.slice(0,indexOf)+text.slice(indexOf+4);
			}	
	};
	/*
	 * The config files which can be translated with the help of the 
	 * translation tool generates the dict with the values having a 
	 * lot of space in the key value pairs. This function strips the 
	 * whitespace.
	 */
	this.stripWhiteSpace = function(dict){
		for (key in dict){
			//Temp fix for section header
			if(key == 'section-header'){
				dict['section-header-read'] = dict[key].replace(/ /g,'_');
				dict['section-header-write'] = dict[key];
			}
			dict[key] = typeof(dict[key]) == 'object' ? that.stripWhiteSpace(dict[key]) : $.trim(dict[key]);
		}
		return dict;
	};
	/*
	 * The function creates the markup for the link to a 
	 * user's user page
	 */
	this.addToInfobox = function(username){
		return username;
	};
	/*
	 * To localize the gadget's interface messages based on the user's language setting
	 */
	this.localizeGadget = function (gadgetClass,localizeDict){
		$(gadgetClass+' [localize]').each(function(){
			var localizeValue = localizeDict[$(this).attr('localize')];
			if($(this).attr('value')){
				$(this).attr('value',localizeValue);
			}
			else if($(this).attr('placeholder')){
				$(this).attr('placeholder',localizeValue);
			}
			else if($(this).attr('data-placeholder')){
				$(this).attr('data-placeholder',localizeValue);
			}
			else{
				$(this).html(localizeValue);
			}
		});
	};
	/*
	 * This function show the feedback speech bubble after an 
	 * endorsement has been made or after joining a project
	 */
	this.showFeedback = function(config,InterfaceMessages){
		var li = $('#'+config['section-header-read']).parent().next().find('li').eq(-1);
		speechBubble = li.append($('<div class="grantsSpeechBubbleContainer"></div>').html('<div class="grantsSpeechBubble">\
		<span localize="message-feedback">Thank You</span></div><div class="grantsSpeechBubbleArrowDown"></div>')).find('.grantsSpeechBubbleContainer');
		var width = li.css('display','inline-block').width();
		li.css('display','');
		li.css('position','relative');
		speechBubble.css('left',width/2+'px');
		$('[localize=message-feedback]').html(InterfaceMessages['message-feedback']);
		$("body, html").animate({ scrollTop : li[0].offsetTop}, that.feedbackScrollTime);
		setTimeout(function(){ speechBubble.hide();},that.feedbackDisappearDelay);
	};
};
/*
 * The Endorse Gadget
 */
var endorseGadget = function(){
	/* Variables */
	var util = new gadgetUtilities();
	var dialog = null;
	
	var api = new mw.Api();	
	var that = this;

	/*
	 * This function creates the dialog box for the gadget.
	 * It is also where all the dialog related interactions are defined.
	 */ 
	var createDialog = function(){
		dialog = $( "<div id='devEndorseDialog'></div>" )
				.html(
					'<div class="mw-ui-vform">\
					 	<div class="error grantsHide" localize="error-save">An error occured</div>\
					 	<div class="error grantsHide" localize="error-login">An error occured</div>\
					 </div>\
					 <div localize="message-description" class="messageDescription">Explaining your endorsement improves process</div>' + '\
					 <textarea rows="5" cols="10" placeholder="Add your comment" id="devEndorseComment" class="" localize="placeholder-comment"></textarea>\
					 <span localize="message-signature" class="messageSignature">Your signature will be added automatically</span>\
					 <div class="gadgetControls">\
						<a href="#" localize="button-cancel" class="mw-ui-button cancel mw-ui-quiet">Cancel</a>\
						<input type="submit" localize="button-submit" class="mw-ui-button mw-ui-constructive add-endorse" disabled localize="button" value="Ok"></input>\
					 </div>'
		).dialog({
				dialogClass: 'grantsGadget endorseGadget',
				autoOpen: false,
				title: 'Endorse Comment',
				width: '495px',
				modal: true,
				closeOnEscape: true,
				resizable: false,
				draggable: false,
				close: function( event, ui ) {
					$('#devEndorseComment').val('');
				}
			});

			$('.add-endorse').click(function(){
				that.addEndorsement(util.cleanupText($('#devEndorseComment').val()));
			});
			
			$('#devEndorseComment').on('change keyup paste',function(){
					util.removeErrorMessage('endorseGadget');
					if($(this).val()){
						$('.add-endorse').attr('disabled',false);
						$('.messageSignature').css('visibility','visible');
					}
					else{
						$('.add-endorse').attr('disabled',true);
						$('.messageSignature').css('visibility','hidden');
					}
			});
			$('.endorseGadget .ui-dialog-title').attr('localize','title');
			
			$('.endorseGadget .cancel').click(function(){
				dialog.dialog('close');
			});
			
			util.localizeGadget('.endorseGadget',that.interfaceMessages);
			
			$('.messageSignature').css('visibility','hidden');
	};
	this.Dialog = function () {
		if (dialog === null){
			createDialog();
		}
		else{
			dialog.dialog('open');
		}
	};
	/*
	 * The main function to add the feedback/endorsement to the page. It first checks if the page has an endorsement section.
	 * If it dosent it creates a new section called Endorsements and appends the feedback/endorsement comment to that section, 
	 * else it appends the feedback/endorsement comment to existing Endorsements section.
	 * The name of the endorsement section is defined in the config. 
	 */
	this.addEndorsement = function( text ) {
		var endorseComment = '\n*' + text + '~~~~' + '\n';
		api.get({
					'format':'json',
					'action':'parse',
					'prop':'sections',
					'page':mw.config.get('wgPageName'),
				}).then(function(result){
					var sections = result.parse.sections;
					var sectionCount = 1;
					var sectionFound = false;
					for (var section in sections ){
						if ($.trim(sections[section]['anchor']) == that.config['section-header-read'] ){
							sectionFound = true;
							break;
						}
						sectionCount++;
					}
					if (sectionFound){
						api.get({
						'format':'json',
						'action':'parse',
						'prop':'wikitext',
						'page': mw.config.get('wgPageName'),
						'section': sectionCount,
						}).then(function(result){
							var wikitext = result.parse.wikitext['*'];
							var endorsementSection = wikitext + endorseComment;
							api.post({
										'action' : 'edit',
										'title' : mw.config.get('wgPageName'),
										'text' : endorsementSection,
										'summary' : 'Endorsed by ' + mw.user.getName(),
										'section': sectionCount,
										'watchlist':'watch',
										'token' : mw.user.tokens.get('csrfToken')
									}).then(function(){
											console.log('Successfully added endorsement');
											window.location.reload(true);
											util.setFeedbackCookie('endorseFeedback');	
									},function(){
										util.showErrorMessage('endorseGadget','save');
										});
							});
					}
					else{
						var sectionHeader = that.config['section-header-write'];
						api.post({
							'action': 'edit',
							'title': mw.config.get('wgPageName'),
							'section': 'new',
							'summary': sectionHeader + ' Endorsed by ' + mw.user.getName(),
							'sectiontitle': sectionHeader,
							'text': $.trim(endorseComment),
							'watchlist':'watch',
							token: mw.user.tokens.get('csrfToken')
						}).then(function () {
								console.log('Successfully added endorsement');
								location.reload();
								util.setFeedbackCookie('endorseFeedback');
							}, function(){
								util.showErrorMessage('endorseGadget','save');
								});
					}			
			}, function(){
				util.showErrorMessage('endorseGadget','save');
				});
	};
};
/*
 * The function the create the join gadget and provides 
 * all the needed functionality.
 */
var joinGadget = function(){
	/* Variables */
	var util = new gadgetUtilities();
	var dialog = null;
	
	this.config = null ;
	this.interfaceMessages = null;
	
	var infobox = '';
	var roleDict = {};
	var api = new mw.Api();	
	var that = this;
	/*
	 * A count is maintained of the open '{{' braces
	 * when a '}}' is encountered the counter is decremented.
	 * If the counter reaches 0 the end of the infobox has been found.
	 * Else the syntax is broken or the end of the infobox is not in 
	 * the first section of the page.
	 */
	var extractInfobox = function(markup){
		var startIndex = markup.indexOf('{{Probox');
		var counter = 0;
		var endIndex = 0;
		for (i=startIndex;i<markup.length;i++){ 
			if(markup[i] == '}' && markup[i+1] == '}'){ 
					counter++;
			} 
			if(markup[i] == '{' && markup[i+1] == '{'){
				counter--;
			} 
			if(counter == 0){
				var endIndex = i+2; 
				break;
			}
		}
		if (counter != 0){
			return '';
		}
		var infobox = { 
			'infobox' : markup.slice(startIndex,endIndex),
		    'before' : markup.slice(0,startIndex),
			'after' : markup.slice(endIndex),
		};
		//return markup.slice(startIndex,endIndex);
		return infobox;
	};
	/*
	 * This function creates the dialog & defines
	 *  needed interactions in the dialog.
	 */ 
	var createDialog = function(){
		dialog = $( "<div id='devJoinDialog'></div>" )
				.html(
					'<div class="mw-ui-vform">\
					 	<div class="error grantsHide" localize="error-save">An error occured</div>\
					 	<div class="error grantsHide" localize="error-login">An error occured</div>\
					 </div>\
					<select class="roleSelect" localize="placeholder-role" data-placeholder="Select a role">\
						<option></option>\
					</select>\
					<div localize="message-description" class="messageDescription">Tell us how you would like to help</div>\
					<textarea rows="5" cols="10" placeholder="Add your comment" id="devJoinComment" class="" localize="placeholder-comment"></textarea>\
					<span localize="message-signature" class="messageSignature">Your signature will be added automatically</span>\
					<div class="gadgetControls">\
						<a href="#" localize="button-cancel" class="mw-ui-button cancel mw-ui-quiet">Cancel</a>\
						<input type="submit" localize="button-join" class="mw-ui-button mw-ui-constructive add-join" disabled localize="button" value="Join"></input>\
					 </div>'
		).dialog({
				dialogClass: 'grantsGadget joinGadget',
				autoOpen: false,
				title: 'join Comment',
				width: '495px',
				modal: true,
				closeOnEscape: true,
				resizable: false,
				draggable: false,
				close: function( event, ui ) {
					$('#devJoinComment').val('');
				}
			});
			$('.add-join').click(function(){
				/*
				 * Creating the comment to add to the participants section. The comment is of the form
				 * "Role" User comment. Eg, Volunteer I can help out in many ways.
				 */
				 
				var joinRole = $('.roleSelect').val().replace(/_/,' ');
				joinRole=joinRole[0].toUpperCase()+joinRole.slice(1);
				joinRole = "'''"+ joinRole + "'''" + " ";
				that.addjoinment(joinRole+util.cleanupText($('#devJoinComment').val()));
			});
			
			$('#devJoinComment').on('change keyup paste',function(){
				util.removeErrorMessage('joinGadget');
					if($(this).val()){
						$('.messageSignature').css('visibility','visible');
						if($('.roleSelect').val()){
							$('.add-join').attr('disabled',false);
						}
					}
					else{
						$('.add-join').attr('disabled',true);
						$('.messageSignature').css('visibility','hidden');
					}
			});
			$('.joinGadget .ui-dialog-title').attr('localize','title');
 
			$('.joinGadget .cancel').click(function(){
				dialog.dialog('close');
			});
			util.localizeGadget('.joinGadget',that.interfaceMessages);
			$('.messageSignature').css('visibility','hidden');
 
			/*
			 * The code below gets the infobox, check for open roles, 
			 * makes sure that these roles are available for other to 
			 * join by looking up roles in the config and creates a drop down 
			 * from which a user can select a role.
			 */
 
			api.get({
						'format':'json',
						'action':'parse',
						'prop':'wikitext',
						'page': mw.config.get('wgPageName'),
						'section': 0
					}).then(function(result){
							var roles = that.interfaceMessages['roles'];
							var wikitext = result.parse.wikitext['*'];
							
							var content = extractInfobox(wikitext);
							var infobox = that.infobox = content['infobox'];
							that.before = content['before'];
							that.after = content['after'];
							units = infobox.split('\n');
							for (unit in units){
								var line = units[unit];
								var role = line.match(/[a-zA-z]+/g);
								if (role){
									role = role.join('');
									var elements = line.split('=');
									var count = elements[0].match(/[0-9]+/)?elements[0].match(/[0-9]+/)[0]:1;
									if (role.indexOf('volunteer') != -1){
										roleDict['volunteer']=count;
									}
									if(role in roles && line.indexOf('=') != -1){
										roleDict[role]=count;
										if(!$('.roleSelect option[value="'+role+'"]').length){											
											$('.roleSelect').append('<option value='+role+'>'+roles[role]+'</option>');
										}
									}
								}
							}
							if(!$('.roleSelect option[value="volunteer"]').length){
								$('.roleSelect').append('<option value="volunteer">'+roles['volunteer']+'</option>');
							}
							$('.roleSelect').chosen({
								disable_search: true,
								placeholder_text_single: 'Select a role',
								width: '50%',
							});
							/* Fix this */
							/*
							$('.roleSelect').on('chosen:showing_dropdown',function(){
								util.removeErrorMessage('endorseGadget');
							});
							*/
							$('.roleSelect').on('change',function(){
								util.removeErrorMessage('joinGadget');
								if($(this).val() && $('#devJoinComment').val()){
									$('.add-join').attr('disabled',false);
								}
								else{
									$('.add-join').attr('disabled',true);
								}
							});
						});		 
	};
	this.Dialog = function () {
		if (dialog === null){
			createDialog();
		}
		else{
			dialog.dialog('open');
		}
	};
	/*
	 * The main function to add the feedback/join comment to the page. It first checks if the page has an Participants section.
	 * If it dosent it creates a new section called Participants and appends the fedback/comment to that section, 
	 * else it appends the feedback/comment to existing Participants section. 
	 */
	this.addjoinment = function( text ) {
		var joinComment = '\n*' + text + '~~~~' + '\n';
		//var joinComment = '*' + text + '~~~~' + '\n';
		//Editing the infobox
		var userName = mw.config.get('wgUserName')?mw.config.get('wgUserName'):'{{subst:REVISIONUSER}}'; 
		var roleSelected = $('.roleSelect').val();
		var units = that.infobox.split('\n');
		var emptyRoleAdded = false;
		for (unit in units){
			if ($.trim(units[unit].split('=')[1]) == ''){			
				var role = units[unit].match(/[a-zA-z]+/);
				if (role){
					role = role[0];
					if(role == roleSelected){
						units[unit] = $.trim(units[unit]) + util.addToInfobox(userName);
						emptyRoleAdded = true;
						break;
					}
				}
			}
		}
		var modifiedInfoBox = units.join("\n");
		if(!emptyRoleAdded){
			var paramCount = roleDict["volunteer"] ? parseInt(roleDict["volunteer"]) + 1 : 1;
			var endIndex = modifiedInfoBox.lastIndexOf('}}');
			modifiedInfoBox = modifiedInfoBox.slice(0,endIndex)+'|volunteer'+paramCount+'='+util.addToInfobox(userName)+'\n}}';
		}
		
		api.post({
			'action' : 'edit',
			'title' : mw.config.get('wgPageName'),
			'text' : that.before + modifiedInfoBox + that.after,
			'summary' : 'Joined as ' + roleSelected,
			'section': 0,
			'watchlist':'watch',
			'token' : mw.user.tokens.get('csrfToken')
		}).then(function(){
			api.get({
					'format':'json',
					'action':'parse',
					'prop':'sections',
					'page':mw.config.get('wgPageName'),
				}).then(function(result){
					var sections = result.parse.sections;
					var sectionCount = 1;
					var sectionFound = false;
					for (var section in sections ){
						if ($.trim(sections[section]['anchor']) == that.config['section-header-read'] ){
							sectionFound = true;
							break;
						}
						sectionCount++;
					}
					if (sectionFound){
						api.get({
							'format':'json',
							'action':'parse',
							'prop':'wikitext',
							'page': mw.config.get('wgPageName'),
							'section': sectionCount
						}).then(function(result){
							var wikitext = result.parse.wikitext['*'];
							var joinmentSection = wikitext + joinComment;
							api.post({
										'action' : 'edit',
										'title' : mw.config.get('wgPageName'),
										'text' : joinmentSection,
										'summary' : 'Adding my name to the participants section',
										'section': sectionCount,
										'watchlist':'watch',
										'token' : mw.user.tokens.get('csrfToken')
									}).then(function(){
										console.log('Successfully added to participants');
										location.reload();
										util.setFeedbackCookie('joinFeedback');
									}, function(){
										util.showErrorMessage('joinGadget','save');
										});
							});
					}
					else{
						var sectionHeader = that.config['section-header-write'];
						api.post({
							'action': 'edit',
							'title': mw.config.get('wgPageName'),
							'section': 'new',
							'summary': sectionHeader,
							'text': $.trim(joinComment),
							'watchlist':'watch',
							'token': mw.user.tokens.get('csrfToken')
						}).then(function () {
								console.log('Successfully added to participants');
								location.reload();
								util.setFeedbackCookie('joinFeedback');
							}, function(){
								util.showErrorMessage('joinGadget','save');
								});
					}			
			}, function(){
				util.showErrorMessage('joinGadget','save');
				});
		}, function(){
			util.showErrorMessage('joinGadget','save');
			}); 
 
	};
};
 
/* End of functions */
mw.loader.using( ['jquery.ui', 'mediawiki.api', 'mediawiki.ui','jquery.chosen'], function() {	
	$(function() {
		(function(){
			var namespace = mw.config.get('wgCanonicalNamespace');
			/*
			 * Fix mw.config.get('wgPageContentLanguage') == 'en') checking with a better solution, 
			 * either when pages can be tagged with arbitary language or when we set langauge markers later on. 
			 * 
			 */
			if (  $('.wp-join-button,.wp-endorse-button').length) {
				if(mw.config.get('wgPageContentLanguage') == 'en'){
					
					var endorse = new endorseGadget();		
					var join = new joinGadget();		
					var util = new gadgetUtilities();
					var api = new mw.Api();
					var interfaceMessagesFullPath = util.interfaceMessagesPath+'/'+util.userLanguage();
					var configFullPath = util.configPath+'/'+util.contentLanguage();
					
					/*
					 * To detect if we have the gadget translations and config in the desired languages.
					 * Currently page language is English always. So the config returned is in en. The InterfaceMessages is
					 * in the user's language
					 */
					api.get({'action':'query','titles':interfaceMessagesFullPath+'|'+configFullPath,'format':'json'}).then(function(data){	
 
						for(id in data.query.pages){
							if (data.query.pages[id].title == util.interfaceMessagesPath+'/'+util.userLanguage() &&id == -1){
								interfaceMessagesFullPath = util.interfaceMessagesPath+'/en';
							}
							if (data.query.pages[id].title == util.configPath+'/'+util.contentLanguage() && id == -1){
								configFullPath = util.configPath+'/en';
							}
						}
 
						var interfaceMessagesUrl = mw.config.get('wgScript')+'?title='+interfaceMessagesFullPath+'&action=raw&ctype=text/javascript';
						var configUrl = mw.config.get('wgScript')+'?title='+configFullPath+'&action=raw&ctype=text/javascript';
						//Get the config for the detected language
						$.when(jQuery.getScript(interfaceMessagesUrl),jQuery.getScript(configUrl)).then(function(){
 							//Stripping Whitespace
 							join.config = util.stripWhiteSpace(util.grantType(joinConfig));
 							join.interfaceMessages = util.stripWhiteSpace(util.grantType(joinInterfaceMessages));
							
							endorse.config = util.stripWhiteSpace(util.grantType(endorseConfig));
							endorse.interfaceMessages = util.stripWhiteSpace(util.grantType(endorseInterfaceMessages));
							
							join.Dialog();
							$('.wp-join-button').unbind();
							$('.wp-join-button').click(function(e){
													e.preventDefault();
													join.Dialog();
												});
							if(util.checkFeedbackCookie('joinFeedback')){
								util.showFeedback(join.config, join.interfaceMessages);
							}
							endorse.Dialog();
							$('.wp-endorse-button').unbind();
							$('.wp-endorse-button').click(function(e){
													e.preventDefault();
													endorse.Dialog();
												});	
							
							//Checking if the user is logged in
							if(!mw.config.get('wgUserName')){
								util.showErrorMessage('endorseGadget','login');
								util.showErrorMessage('joinGadget','login');	
							}
							
							if(util.checkFeedbackCookie('endorseFeedback')){
								util.showFeedback(endorse.config, endorse.interfaceMessages);
							}	
						});
					});
				}
				else{
					$('.wp-join-button').hide();
					$('.wp-endorse-button').hide();
				}
			}
		})();
	}); 
});
 
//</nowiki>