User:Wrh2/Gadget-ListingEditor-it.js

/******************************************************************  Listing Editor v2.0 Original author: - torty3 Additional contributors: - Andyrom75 - Wrh2 v2.0 Changes: - Update the listing editor dialog UI to provide more space for field entry, and to responsively collapse from two columns to one on small screens. - Use mw.Api.postWithToken instead of $.ajax to hopefully fix session token expiration issues. - Add support for editing of multi-paragraph listings. - Do not delete non-empty unrecognized listing template values (wikipedia,      phoneextra, etc) when editing if the ALLOW_UNRECOGNIZED_PARAMETERS flag is set to true. - If listing editor form submit fails, re-display the listing editor form with the content entered by the user so that work is not lost. - Replace synchronous $.ajax call with asynchronous. - Allow edit summaries and marking edits as minor when editing listings. - Move 'add listing' link within the mw-editsection block. - Automatically replace newlines in listing content with tags. - Fix bug where HTML comments inside listing fields prevented editing. - Provide configuration options so that some fields can be displayed only if they have non-empty values (examples: "fax" in the default      configuration). - Add basic email address validation. - A failed captcha challenge no longer causes further captcha attempts to      fail. - Significant code reorganization. TODO - Add support for mobile devices. - Handle "=" in listing parameter values (")" - Add support for non-standard listing "type" values ("go", etc). - wrapContent is breaking the expand/collapse logic on the VFD page. - populate the input-type select list from LISTING_TEMPLATES //

( function ( mw, $ ) {	'use strict';

/* ***********************************************************************	 * CUSTOMIZATION INSTRUCTIONS: *	 * Different Wikivoyage language versions have different implementations of * the listing template, so this module must be customized for each. The * ListingEditor.Config and ListingEditor.Callbacks modules should be the * ONLY code that requires customization - ListingEditor.Core should be * shared across all language versions. If for some reason the Core module * must be modified, ideally the module should be modified for all language * versions so that the code can stay in sync. * ***********************************************************************/

var ListingEditor = {};

// see http://toddmotto.com/mastering-the-module-pattern/ for an overview // of the module design pattern being used in this gadget

/* ***********************************************************************	 * ListingEditor.Config contains properties that will likely need to be * modified for each Wikivoyage language version. Properties in this * module will be referenced from the other ListingEditor modules. * ***********************************************************************/	ListingEditor.Config = function {

// 		// TRANSLATE THE FOLLOWING BASED ON THE WIKIVOYAGE LANGUAGE IN USE //

var LANG = 'it'; var TRANSLATIONS = { 'addTitle' : 'aggiungi elemento', 'editTitle' : 'modifica', 'add': 'aggiungi elemento', 'edit': 'modifica', 'saving': 'Salvataggio...', 'submit': 'Salva', 'cancel': 'Annulla', 'validationEmptyListing': 'Inserisci almeno un\'informazione tra nome o indirizzo', 'validationEmail': 'Please ensure the email address is valid', 'added': 'Aggiunto elemento: ', 'updated': 'Aggiornato elemento: ', 'removed': 'Rimosso elemento: ', 'helpPage': '//en.wikivoyage.org/wiki/Wikivoyage:Listing_editor', 'enterCaptcha': 'Inserisci il CAPTCHA', 'externalLinks': 'La tua modifica include nuovi collegamenti esterni.', // license text should match MediaWiki:Wikimedia-copyrightwarning 'licenseText': 'Facendo click su "Salva", accetti espressamente i Termini d\'uso, e accetti irrevocabilmente a rilasciare il tuo contributo sotto la licenza CC-BY-SA 3.0.', 'ajaxInitFailure': 'Error: Unable to initialize the listing editor', 'submitApiError': 'Errore: API ha ritornato il seguente codice d\'errore', 'submitBlacklistError': 'Errore: A value in the data submitted has been blacklisted, please remove the blacklisted pattern and try again', 'submitUnknownError': 'Errore: Risultato sconosciuto dell\'API.', 'submitHttpError': 'Errore: Richiesta fallita.', 'submitEmptyError': 'Errore: Richiesta fallita.', 'budget': 'Prezzi modici', 'midrange': 'Prezzi medi', 'splurge': 'Prezzi elevati' };

// 		// CONFIGURE THE FOLLOWING BASED ON WIKIVOYAGE COMMUNITY PREFERENCES //

// if the browser window width is less than MAX_DIALOG_WIDTH (pixels), the // listing editor dialog will fill the available space, otherwise it will // be limited to the specified width var MAX_DIALOG_WIDTH = 1200; // set this flag to false if the listing editor should strip away any // listing template parameters that are not explicitly configured in the // LISTING_TEMPLATES parameter arrays (such as wikipedia, phoneextra, etc). // if the flag is set to true then unrecognized parameters will be allowed // as long as they have a non-empty value. var ALLOW_UNRECOGNIZED_PARAMETERS = false;

// 		// UPDATE THE FOLLOWING TO MATCH WIKIVOYAGE ARTICLE SECTION NAMES //

// map section heading ID to the listing template to use for that section var SECTION_TO_TEMPLATE_TYPE = { 'Cosa_vedere': 'see', 'Cosa_fare': 'do', 'Acquisti': 'buy', 'Dove_mangiare': 'eat', 'Come_divertirsi': 'drink', 'Dove_alloggiare': 'sleep', 'Eventi_e_feste': 'listing' };		// If any of these patterns are present on a page then no 'add listing' // buttons will be added to the page var DISALLOW_ADD_LISTING_IF_PRESENT = ['#Centri_urbani', '#Altre_destinazioni', '#Islands', '#print-districts' ];

// 		// CONFIGURE THE FOLLOWING TO MATCH THE LISTING TEMPLATE PARAMS & OUTPUT //

// name of the generic listing template to use when a more specific // template ("see", "do", etc) is not appropriate var DEFAULT_LISTING_TEMPLATE = 'listing'; var LISTING_TYPE_PARAMETER = 'tipo'; var LISTING_CONTENT_PARAMETER = 'descrizione'; // selector that identifies the HTML elements into which the 'edit' link // for each listing will be placed // TODO - IT needs to modify the listing template to add a span that will contain the edit link var EDIT_LINK_CONTAINER_SELECTOR = 'span.listing-metadata-items'; // The arrays below must include entries for each listing template // parameter in use for each Wikivoyage language version - for example // "name", "address", "phone", etc. If all listing template types use // the same parameters then a single configuration array is sufficient, // but if listing templates use different parameters or have different // rules about which parameters are required then the differences must // be configured - for example, English Wikivoyage uses "checkin" and // "checkout" in the "sleep" template, so a separate // SLEEP_TEMPLATE_PARAMETERS array has been created below to define the // different requirements for that listing template type. //		// Once arrays of parameters are defined, the LISTING_TEMPLATES // mapping is used to link the configuration to the listing template // type, so in the English Wikivoyage example all listing template // types use the LISTING_TEMPLATE_PARAMETERS configuration EXCEPT for // "sleep" listings, which use the SLEEP_TEMPLATE_PARAMETERS // configuration. //		// Fields that can used in the configuration array(s): //  - id: HTML input ID in the EDITOR_FORM_HTML for this element. //  - hideDivIfEmpty: id of a in the EDITOR_FORM_HTML for this //    element that should be hidden if the corresponding template //    parameter has no value. For example, the "fax" field is		//    little-used and is not shown by default in the editor form if it //    does not already have a value. //  - skipIfEmpty: Do not include the parameter in the wiki template //    syntax that is saved to the article if the parameter has no //    value. For example, the "image" tag is not included by default //    in the listing template syntax unless it has a value. //  - newline: Append a newline after the parameter in the listing //    template syntax when the article is saved. var LISTING_TEMPLATE_PARAMETERS = { 'tipo': { id:'input-type', hideDivIfEmpty: 'div_type', newline: true }, 'nome': { id:'input-name' }, 'alt': { id:'input-alt' }, 'sito': { id:'input-url' }, 'email': { id:'input-email', newline: true }, 'indirizzo': { id:'input-address' }, 'lat': { id:'input-lat' }, 'long': { id:'input-long' }, 'indicazioni': { id:'input-directions', newline: true }, 'tel': { id:'input-phone' }, 'numero verde': { id:'input-tollfree' }, 'fax': { id:'input-fax' }, 'immagine': { id:'input-image', hideDivIfEmpty: 'div_image', skipIfEmpty: true, newline: true }, 'orari': { id:'input-hours' }, 'checkin': { id:'input-checkin', hideDivIfEmpty: 'div_checkin', skipIfEmpty: true }, 'checkout': { id:'input-checkout', hideDivIfEmpty: 'div_checkout', skipIfEmpty: true }, 'prezzo': { id:'input-price', newline: true }, // 'lastedit': { id:'input-lastedit', newline: true, skipIfEmpty: true }, 'descrizione': { id:'input-content', newline: true } };		// override the default settings for "sleep" listings since that // listing type uses "checkin"/"checkout" instead of "hours" var SLEEP_TEMPLATE_PARAMETERS = $.extend(true, {}, LISTING_TEMPLATE_PARAMETERS, {			'orari': { hideDivIfEmpty: 'div_hours', skipIfEmpty: true },			'checkin': { hideDivIfEmpty: null, skipIfEmpty: false },			'checkout': { hideDivIfEmpty: null, skipIfEmpty: false }		}); // map the template name to configuration information needed by the listing // editor var LISTING_TEMPLATES = { 'listing': LISTING_TEMPLATE_PARAMETERS, 'see': LISTING_TEMPLATE_PARAMETERS, 'do': LISTING_TEMPLATE_PARAMETERS, 'buy': LISTING_TEMPLATE_PARAMETERS, 'eat': LISTING_TEMPLATE_PARAMETERS, 'drink': LISTING_TEMPLATE_PARAMETERS, 'sleep': SLEEP_TEMPLATE_PARAMETERS };

// 		// CONFIGURE THE FOLLOWING TO IMPLEMENT THE UI FOR THE LISTING EDITOR //

// these selectors should match a value defined in the EDITOR_FORM_HTML // if the selector refers to a field that is not used by a Wikivoyage // language version the variable should still be defined, but the // corresponding element in EDITOR_FORM_HTML can be removed and thus // the selector will not match anything and the functionality tied to // the selector will never execute. var EDITOR_FORM_SELECTOR = '#listing-editor'; var EDITOR_CLOSED_SELECTOR = '#input-closed'; var EDITOR_SUMMARY_SELECTOR = '#input-summary'; var EDITOR_MINOR_EDIT_SELECTOR = '#input-minor'; // the below HTML is the UI that will be loaded into the listing editor // dialog box when a listing is added or edited. EACH WIKIVOYAGE // LANGUAGE SITE CAN CUSTOMIZE THIS HTML - fields can be removed, // added, displayed differently, etc. Note that it is important that // any changes to the HTML structure are also made to the // LISTING_TEMPLATES parameter arrays since that array provides the // mapping between the editor HTML and the listing template fields. var EDITOR_FORM_HTML = '' + '' + ' ' +			' ' +			'' + ' ' +			' ' +			' ' +			// update the ListingEditor.Callbacks.hideEditOnlyFields method if			// the summary table is removed or modified ' ' +			' ';

// expose public members return { LANG: LANG, TRANSLATIONS: TRANSLATIONS, EDIT_LINK_CONTAINER_SELECTOR: EDIT_LINK_CONTAINER_SELECTOR, MAX_DIALOG_WIDTH: MAX_DIALOG_WIDTH, DISALLOW_ADD_LISTING_IF_PRESENT: DISALLOW_ADD_LISTING_IF_PRESENT, DEFAULT_LISTING_TEMPLATE: DEFAULT_LISTING_TEMPLATE, LISTING_TYPE_PARAMETER: LISTING_TYPE_PARAMETER, LISTING_CONTENT_PARAMETER: LISTING_CONTENT_PARAMETER, ALLOW_UNRECOGNIZED_PARAMETERS: LISTING_CONTENT_PARAMETER, SECTION_TO_TEMPLATE_TYPE: SECTION_TO_TEMPLATE_TYPE, LISTING_TEMPLATES: LISTING_TEMPLATES, EDITOR_FORM_SELECTOR: EDITOR_FORM_SELECTOR, EDITOR_CLOSED_SELECTOR: EDITOR_CLOSED_SELECTOR, EDITOR_SUMMARY_SELECTOR: EDITOR_SUMMARY_SELECTOR, EDITOR_MINOR_EDIT_SELECTOR: EDITOR_MINOR_EDIT_SELECTOR, EDITOR_FORM_HTML: EDITOR_FORM_HTML };	};

/* ***********************************************************************	 * ListingEditor.Callbacks implements custom functionality that may be	 * specific to how a Wikivoyage language version has implemented the * listing template. For example, English Wikivoyage uses a "last edit" * date that needs to be populated when the listing editor form is	 * submitted, and that is done via custom functionality implemented as a	 * SUBMIT_FORM_CALLBACK function in this module. * ***********************************************************************/	ListingEditor.Callbacks = function { // array of functions to invoke when creating the listing editor form. // these functions will be invoked with the form DOM object as the // first element and the mode as the second element. var CREATE_FORM_CALLBACKS = []; // array of functions to invoke when submitting the listing editor // form but prior to validating the form. these functions will be		// invoked with the mapping of listing attribute to value as the first // element and the mode as the second element. var SUBMIT_FORM_CALLBACKS = []; // array of validation functions to invoke when the listing editor is // submitted. these functions will be invoked with an array of		// validation messages as an argument; a failed validation should add a		// message to this array, and the user will be shown the messages and // the form will not be submitted if the array is not empty. var VALIDATE_FORM_CALLBACKS = [];

// 		// LISTING EDITOR UI INITIALIZATION CALLBACKS //

/**		 * Add listeners to the currency symbols so that clicking on a currency * symbol will insert it into the price input. */		var initCurrencySymbolFormFields = function(form, mode) { var CURRENCY_SIGNS_SELECTOR = '.currency-signs'; $(CURRENCY_SIGNS_SELECTOR, form).click(function {				var priceInput = $('#input-price');				var caretPos = priceInput[0].selectionStart;				var oldPrice = priceInput.val;				var currencySymbol = $(this).find('a').text;				var newPrice = oldPrice.substring(0, caretPos) + currencySymbol + oldPrice.substring(caretPos);				priceInput.val(newPrice);			}); };		CREATE_FORM_CALLBACKS.push(initCurrencySymbolFormFields);

/**		 * Add listeners on various fields to update the "find on map" link. */		var initFindOnMapLink = function(form, mode) { var latlngStr = '?lang=' + ListingEditor.Config.LANG; latlngStr += '&page=' + encodeURIComponent(mw.config.get('wgTitle')); // #geodata should be a hidden span added by Template:Geo // containing the lat/long coordinates of the destination if ($('#geodata').length) { var latlng = $('#geodata').text.split('; '); latlngStr += '&lat=' + latlng[0] + '&lon=' + latlng[1] + '&zoom=15'; }			if ($('#input-address', form).val !== '') { latlngStr += '&location=' + encodeURIComponent($('#input-address', form).val); } else if ($('#input-name', form).val !== '') { latlngStr += '&location=' + encodeURIComponent($('#input-name', form).val); }			// #geomap-link is a link in the EDITOR_FORM_HTML $('#geomap-link', form).attr('href', $('#geomap-link', form).attr('href') + latlngStr); $('#input-address', form).change( function {				var link = $('#geomap-link').attr('href');				var index = link.indexOf('&location');				if (index < 0) index = link.length;				$('#geomap-link').attr('href', link.substr(0,index) + '&location=' + encodeURIComponent($('#input-address').val));			}); };		CREATE_FORM_CALLBACKS.push(initFindOnMapLink);

var hideEditOnlyFields = function(form, mode) { var EDITOR_STATUS_ROW = '#div_status'; var EDITOR_SUMMARY_ROW = '#div_summary'; if (mode !== ListingEditor.Core.MODE_EDIT) { $(EDITOR_STATUS_ROW, form).hide; $(EDITOR_SUMMARY_ROW, form).hide; }		};		CREATE_FORM_CALLBACKS.push(hideEditOnlyFields);

// 		// LISTING EDITOR FORM SUBMISSION CALLBACKS //

/**		 * Return the current date in the format "2015-01-15". */		var currentLastEditDate = function { var d = new Date; var year = d.getFullYear; // Date.getMonth returns 0-11 var month = d.getMonth + 1; if (month < 10) month = '0' + month; var day = d.getDate; if (day < 10) day = '0' + day; return year + '-' + month + '-' + day; };

/**		 * Only update last edit date if this is a new listing or if the * "information up-to-date" box checked. */		var updateLastEditDate = function(listing, mode) { var LISTING_LAST_EDIT_PARAMETER = 'lastedit'; var EDITOR_LAST_EDIT_SELECTOR = '#input-last-edit'; if (mode == ListingEditor.Core.MODE_ADD || $(EDITOR_LAST_EDIT_SELECTOR).is(':checked')) { listing[LISTING_LAST_EDIT_PARAMETER] = currentLastEditDate; }		};		//SUBMIT_FORM_CALLBACKS.push(updateLastEditDate);

// 		// LISTING EDITOR FORM VALIDATION CALLBACKS //

/**		 * Verify all listings have at least a name, address or alt value. */		var validateListingHasData = function(validationFailureMessages) { if ($('#input-name').val ===  && $('#input-address').val ===  && $('#input-alt').val === '') { validationFailureMessages.push(ListingEditor.Config.TRANSLATIONS.validationEmptyListing); }		};		VALIDATE_FORM_CALLBACKS.push(validateListingHasData);

/**		 * Implement SIMPLE validation on email addresses. Invalid emails can * still get through, but this method implements a minimal amount of * validation in order to catch the worst offenders. */		var validateEmail = function(validationFailureMessages) { var VALID_EMAIL_REGEX = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; var email = $('#input-email').val.trim; if (email !== '' && !VALID_EMAIL_REGEX.test(email)) { validationFailureMessages.push(ListingEditor.Config.TRANSLATIONS.validationEmail); }		};		VALIDATE_FORM_CALLBACKS.push(validateEmail);

// expose public members return { CREATE_FORM_CALLBACKS: CREATE_FORM_CALLBACKS, SUBMIT_FORM_CALLBACKS: SUBMIT_FORM_CALLBACKS, VALIDATE_FORM_CALLBACKS: VALIDATE_FORM_CALLBACKS };	};

/* ***********************************************************************	 * ListingEditor.Core contains code that should be shared across different * Wikivoyage languages. This code uses the custom configurations in the * ListingEditor.Config and ListingEditor.Callback modules to initialize * the listing editor and process add and update requests for listings. * ***********************************************************************/	ListingEditor.Core = function { var api = new mw.Api; var MODE_ADD = 'add'; var MODE_EDIT = 'edit'; // selector that identifies the edit link as created by the // addEditButtons function var EDIT_LINK_SELECTOR = '.vcard-edit-button'; var SAVE_FORM_SELECTOR = '#progress-dialog'; var CAPTCHA_FORM_SELECTOR = '#captcha-dialog'; var sectionText, inlineListing, replacements = {};

/**		 * Return false if the current page should not enable the listing editor. * Examples where the listing editor should not be enabled include talk * pages, edit pages, history pages, etc.		 */ var listingEditorAllowedForCurrentPage = function { var namespace = mw.config.get( 'wgNamespaceNumber' ); if (namespace !== 0 && namespace !== 2 && namespace !== 4) { return false; }			if ( mw.config.get('wgAction') != 'view' || $('#mw-revision-info').length					|| mw.config.get('wgCurRevisionId') != mw.config.get('wgRevisionId')					|| $('#ca-viewsource').length ) { return false; }			return true; };

/**		 * Generate the form UI for the listing editor. If editing an existing * listing, pre-populate the form input fields with the existing values. */		var createForm = function(mode, listingParameters, listingTemplateAsMap) { var form = $(ListingEditor.Config.EDITOR_FORM_HTML); // populate the empty form with existing values for (var parameter in listingParameters) { var parameterInfo = listingParameters[parameter]; if (listingTemplateAsMap[parameter]) { $('#' + parameterInfo.id, form).val(listingTemplateAsMap[parameter]); } else if (parameterInfo.hideDivIfEmpty) { $('#' + parameterInfo.hideDivIfEmpty, form).hide; }			}			for (var i=0; i < ListingEditor.Callbacks.CREATE_FORM_CALLBACKS.length; i++) { ListingEditor.Callbacks.CREATE_FORM_CALLBACKS[i](form, mode); }			return form; };

/**		 * Wrap the h2/h3 heading tag and everything up to the next section * (including sub-sections) in a div to make it easier to traverse the DOM. * This change introduces the potential for code incompatibility should the * div cause any CSS or UI conflicts. */		var wrapContent = function { $('#bodyContent h2').each(function{				$(this).nextUntil("h1, h2").addBack.wrapAll(' ');			}); $('#bodyContent h3').each(function{				$(this).nextUntil("h1, h2, h3").addBack.wrapAll(' ');			}); };

/**		 * Place an "add listing" link at the top of each section heading next to * the "edit" link in the section heading. */		var addListingButtons = function { if ($(ListingEditor.Config.DISALLOW_ADD_LISTING_IF_PRESENT.join(',')).length > 0) { return false; }			for (var sectionId in ListingEditor.Config.SECTION_TO_TEMPLATE_TYPE) { sectionId = encodeURIComponent(sectionId).replace(/%20/g,'_').replace(/%/g,'.'); // use a pattern rather than searching by ID directly in case // the article uses top-level headings in sub-sections var topHeading = $('h2 #' + sectionId); if (topHeading.length) { insertAddListingPlaceholder(topHeading); var parentHeading = topHeading.closest('div.mw-h2section'); $('h3 .mw-headline', parentHeading).each(function {						insertAddListingPlaceholder(this);					}); }			}			$('.listingeditor-add').click(function {				initListingEditorDialog(MODE_ADD, $(this));			}); };

/**		 * Utility function for appending the "add listing" link text to a heading. */		var insertAddListingPlaceholder = function(parentHeading) { var editSection = $(parentHeading).next('.mw-editsection'); editSection.append(' [ '+ListingEditor.Config.TRANSLATIONS.add+' ] '); };

/**		 * Place an "edit" link next to all existing listing tags. */		var addEditButtons = function { var editButton = $(' ') .html(''+ListingEditor.Config.TRANSLATIONS.edit+'' ) .click(function {					initListingEditorDialog(MODE_EDIT, $(this));				}); // if there is already metadata present add a separator $(ListingEditor.Config.EDIT_LINK_CONTAINER_SELECTOR).each(function {					if (!isElementEmpty(this)) {						$(this).append(' | ');					}				}); // append the edit link $(ListingEditor.Config.EDIT_LINK_CONTAINER_SELECTOR).append( editButton ); };

/**		 * Determine whether a listing entry is within a paragraph rather than * an entry in a list; inline listings will be formatted slightly * differently than entries in lists (no newlines in the template syntax,		 * skip empty fields). */		var isInline = function(entry) { return (entry.closest('ul, dl, ol').length === 0); };

/**		 * Given a DOM element, find the nearest editable section (h2 or h3) that * it is contained within. */		var findSectionHeading = function(element) { // first see if there is a subsection match nearby var heading = element.closest('div.mw-h3section'); if (heading.length === 0) { // if no subsection match found, look for the nearest major section heading = element.closest('div.mw-h2section'); }			return heading; };

/**		 * Given an editable heading, examine it to determine what section index * the heading represents. First heading is 1, second is 2, etc.		 */ var findSectionIndex = function(heading) { if (heading === undefined) { return 0; }			var link = heading.find('.mw-editsection a').attr('href'); return (link !== undefined) ? link.split('=').pop : 0; };

/**		 * Given an edit link that was clicked for a listing, determine what index * that listing is within a section. First listing is 0, second is 1, etc.		 */ var findListingIndex = function(sectionHeading, clicked) { var count = 0; $(EDIT_LINK_SELECTOR, sectionHeading).each(function {				if (clicked.is($(this))) {					return false;				}				count++;			}); return count; };

/**		 * Return the listing template type appropriate for the section that * contains the provided DOM element (example: "see" for "See" sections,		 * etc). If no matching type is found then the default listing template * type is returned. */		var findListingTypeForSection = function(entry) { var sectionType = entry.closest('div.mw-h2section').children('h2').find('.mw-headline').attr('id'); for (var sectionId in ListingEditor.Config.SECTION_TO_TEMPLATE_TYPE) { if (sectionType == sectionId) { return ListingEditor.Config.SECTION_TO_TEMPLATE_TYPE[sectionId]; }			}			return ListingEditor.Config.DEFAULT_LISTING_TEMPLATE; };

var replaceSpecial = function(str) { return str.replace(/([.?*+^$[\]\\{}|-])/g, "\\$1"); };

/**		 * Return a regular expression that can be used to find all listing * template invocations (as configured via the LISTING_TEMPLATES map) * within a section of wikitext. Note that the returned regex simply * matches the start of the template ("{{listing") and not the full * template ("{{listing|key=value|...}}"). */		var getListingTypesRegex = function { var regex = []; for (var key in ListingEditor.Config.LISTING_TEMPLATES) { regex.push(key); }			return new RegExp('{{\\s*(' + regex.join('|') + ')\\b','g'); };

/**		 * Given a listing index, return the full wikitext for that listing * ("{{listing|key=value|...}}"). An index of 0 returns the first listing * template invocation, 1 returns the second, etc.		 */ var getListingWikitextBraces = function(listingIndex) { sectionText = sectionText.replace(/[^\S\n]+/g,' '); // find the listing wikitext that matches the same index as the listing index var listingRegex = getListingTypesRegex; // look through all matches for "{{listing|see|do...}}" within the section // wikitext, returning the nth match, where 'n' is equal to the index of the // edit link that was clicked var string, match, index; for (var i = 0; i <= listingIndex; i++) { match = listingRegex.exec(sectionText); index = match.index; string = match[0]; }			// listings may contain nested templates, so check for MATCHING closing // braces for this listing var curly = 2; var str1 = , str2 = ; // search for open and close braces for (var i=index; i>0; i--) { if (sectionText[i] === '}') { ++curly; } else if (sectionText[i] === '{') { --curly; }				if (curly === 0) { str1 = sectionText.substr(i,index-i); break; }			}			if (string.indexOf('}}') < 0) { curly = 2; }			var textLength = sectionText.length; for (var j=index+string.length; j<textLength; j++) { if (sectionText[j] === '{') { ++curly; } else if (sectionText[j] === '}') { --curly; }				if (curly === 0) { str2 = sectionText.substr(index, j-index+1); break; }			}			if (str2 === '') str2 = sectionText.substr(index, textLength); string = str1 + str2; return $.trim(string); };

/**		 * Convert raw wiki listing syntax into a mapping of key-value pairs * corresponding to the listing template parameters. */		var wikiTextToListing = function(listingTemplateWikiSyntax) { var typeRegex = getListingTypesRegex; // remove the trailing braces listingTemplateWikiSyntax = listingTemplateWikiSyntax.slice(0,-2); // convert "{{see" to {{listing|type=see"			listingTemplateWikiSyntax = listingTemplateWikiSyntax.replace(typeRegex,'{{listing| ' + ListingEditor.Config.LISTING_TYPE_PARAMETER + '=$1');			var listingTemplateAsMap = {};			var lastKey;			var listParams = listingTemplateToParamsArray(listingTemplateWikiSyntax);			for (var j=1; j < listParams.length; j++) {				var param = listParams[j];				var index = param.indexOf('=');				if (index > 0) {					// param is of the form key=value					var key = $.trim(param.substr(0, index));					var value = $.trim(param.substr(index+1));					listingTemplateAsMap[key] = value;					lastKey = key;				} else if (listingTemplateAsMap[lastKey].length) {					// there was a pipe character within a param value, such as					// "key=value1|value2", so just append to the previous param					listingTemplateAsMap[lastKey] += '|' + param;				}			}			for (var key in listingTemplateAsMap) { // if the template value contains an HTML comment that was // previously converted to a placehold then it needs to be				// converted back to a comment so that the placeholder is not // displayed in the edit form listingTemplateAsMap[key] = restoreComments(listingTemplateAsMap[key], false); }			if (listingTemplateAsMap[ListingEditor.Config.LISTING_CONTENT_PARAMETER]) { // convert paragraph tags to newlines so that the content is more // readable in the editor window listingTemplateAsMap[ListingEditor.Config.LISTING_CONTENT_PARAMETER] = listingTemplateAsMap[ListingEditor.Config.LISTING_CONTENT_PARAMETER].replace(/\s* \s*/g, '\n\n'); }			return listingTemplateAsMap; };

/**		 * Split the wikitext into an array of params. This code exists in its * own method primarily to allow it to be overridden by Italian * Wikivoyage. */		var listingTemplateToParamsArray = function(listingTemplateWikiSyntax) { /* +++ BEGIN IT CUSTOM LOGIC +++ */ var OpenBracket = 0; var OpenSquareBrackets = 0; var listParams = []; var buff = ''; for ( var i = 0; i < listingTemplateWikiSyntax.length; i++ ) { var chr = listingTemplateWikiSyntax.charAt(i); if ( chr === '{' ) { OpenBracket++; } else if ( chr === '}' ) { OpenBracket--; } else if ( chr === '[' ) { OpenSquareBrackets++; } else if ( chr === ']' ) { OpenSquareBrackets--; }				if ( chr === '|' && OpenBracket === 2 && OpenSquareBrackets === 0 ) { listParams.push(buff); buff = ''; } else { buff += chr; }			}			if ( buff !== '' ) { listParams.push(buff); buff = ''; }			return listParams; /* +++ END IT CUSTOM LOGIC +++ */ };

/**		 * This method is invoked when an "add" or "edit" listing button is		 * clicked and will execute an Ajax request to retrieve all of the raw wiki * syntax contained within the specified section. This wiki text will * later be modified via the listing editor and re-submitted as a section * edit. */		var initListingEditorDialog = function(mode, clicked) { var listingType; if (mode === MODE_ADD) { listingType = findListingTypeForSection(clicked); }			var sectionHeading = findSectionHeading(clicked); var sectionIndex = findSectionIndex(sectionHeading); var listingIndex = (mode === MODE_ADD) ? -1 : findListingIndex(sectionHeading, clicked); inlineListing = (mode === MODE_EDIT && isInline(clicked)); $.ajax({				url: mw.util.wikiScript(''),				data: { title: mw.config.get('wgPageName'), action: 'raw', section: sectionIndex },				cache: false // required			}).done(function(data, textStatus, jqXHR) {				sectionText = data;				openListingEditorDialog(mode, sectionIndex, listingIndex, listingType);			}).fail(function(jqXHR, textStatus, errorThrown) {				alert(ListingEditor.Config.TRANSLATIONS.ajaxInitFailure + ': ' + textStatus + ' ' + errorThrown);			}); };

/**		 * This method is called asynchronously after the initListingEditorDialog * method has retrieved the existing wiki section content that the * listing is being added to (and that contains the listing wiki syntax		 * when editing). */		var openListingEditorDialog = function(mode, sectionNumber, listingIndex, listingType) { sectionText = stripComments(sectionText); mw.loader.using( ['jquery.ui'], function {				var listingTemplateAsMap, listingTemplateWikiSyntax;				if (mode == MODE_ADD) {					listingTemplateAsMap = {};					listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER] = listingType;				} else {					listingTemplateWikiSyntax = getListingWikitextBraces(listingIndex);					listingTemplateAsMap = wikiTextToListing(listingTemplateWikiSyntax);					listingType = listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER];				}				var listingParameters = getListingInfo(listingType);				// if a listing editor dialog is already open, get rid of it				if ($(ListingEditor.Config.EDITOR_FORM_SELECTOR).length > 0) {					$(ListingEditor.Config.EDITOR_FORM_SELECTOR).dialog('destroy').remove;				}				var form = $(createForm(mode, listingParameters, listingTemplateAsMap));				// wide dialogs on huge screens look terrible				var windowWidth = $(window).width; var dialogWidth = (windowWidth > ListingEditor.Config.MAX_DIALOG_WIDTH) ? ListingEditor.Config.MAX_DIALOG_WIDTH : 'auto'; // modal form - must submit or cancel form.dialog({					modal: true,					height: 'auto',					width: dialogWidth,					title: (mode == MODE_ADD) ? ListingEditor.Config.TRANSLATIONS.addTitle : ListingEditor.Config.TRANSLATIONS.editTitle,					dialogClass: 'listing-editor-dialog',					buttons: [					{						text: '?',						id: 'listing-help',						click: function { window.open(ListingEditor.Config.TRANSLATIONS.helpPage);}					},					{						text: ListingEditor.Config.TRANSLATIONS.submit, click: function {							if (validateForm) {								formToText(mode, listingTemplateWikiSyntax, listingTemplateAsMap, sectionNumber);								$(this).dialog('close');							}						}					},					{						text: ListingEditor.Config.TRANSLATIONS.cancel,						click: function {							$(this).dialog('destroy').remove;						}					}					],					create: function {						$('.ui-dialog-buttonpane').append(' ' + ListingEditor.Config.TRANSLATIONS.licenseText + ' '); }				});			});		};

/**		 * Commented-out listings can result in the wrong listing being edited, so		 * strip out any comments and replace them with placeholders that can be * restored prior to saving changes. */		var stripComments = function(text) { var comments = text.match(//mig); if (comments !== null ) { for (var i = 0; i < comments.length; i++) { var comment = comments[i]; var rep = '<<>>'; text = text.replace(comment, rep); replacements[rep] = comment; }			}			return text; };

/**		 * Search the text provided, and if it contains any text that was * previously stripped out for replacement purposes, restore it. */		var restoreComments = function(text, resetReplacements) { for (var key in replacements) { var val = replacements[key]; text = text.replace(key, val); }			if (resetReplacements) { replacements = {}; }			return text; };

/**		 * Given a listing type, return the appropriate entry from the * LISTING_TEMPLATES array. This method returns the entry for the default * listing template type if not enty exists for the specified type. */		var getListingInfo = function(type) { return (ListingEditor.Config.LISTING_TEMPLATES[type]) ? ListingEditor.Config.LISTING_TEMPLATES[type] : ListingEditor.Config.LISTING_TEMPLATES[ListingEditor.Config.DEFAULT_LISTING_TEMPLATE]; };

/**		 * Logic invoked on form submit to analyze the values entered into the * editor form and to block submission if any fatal errors are found. */		var validateForm = function { var validationFailureMessages = []; for (var i=0; i < ListingEditor.Callbacks.VALIDATE_FORM_CALLBACKS.length; i++) { ListingEditor.Callbacks.VALIDATE_FORM_CALLBACKS[i](validationFailureMessages); }			if (validationFailureMessages.length > 0) { alert(validationFailureMessages.join('\n')); return false; }			// newlines in listing content won't render properly in lists, so			// replace them with tags $('#input-content').val($.trim($('#input-content').val).replace(/\n+/g, ' ')); var webRegex = new RegExp('^https?://', 'i'); var url = $('#input-url').val; if (!webRegex.test(url) && url !== '') { $('#input-url').val('http://' + url); }			return true; };

/**		 * Capitalize the first character of the given string. */		var upperCaseFirst = function(str) { str = str.toLowerCase.replace(/\b[a-z]/g, function(letter) {				return letter.toUpperCase;			}); return str; };

/**		 * Convert the listing editor form entry fields into wiki text. This * method converts the form entry fields into a listing template string, * replaces the original template string in the section text with the * updated entry, and then submits the section text to be saved on the * server. */		var formToText = function(mode, listingTemplateWikiSyntax, listingTemplateAsMap, sectionNumber) { var listing = listingTemplateAsMap; var defaultListingParameters = getListingInfo(ListingEditor.Config.DEFAULT_LISTING_TEMPLATE); var listingTypeInput = defaultListingParameters[ListingEditor.Config.LISTING_TYPE_PARAMETER].id; var listingType = $("#" + listingTypeInput).val; var listingParameters = getListingInfo(listingType); for (var parameter in listingParameters) { listing[parameter] = $("#" + listingParameters[parameter].id).val; }			for (var i=0; i < ListingEditor.Callbacks.SUBMIT_FORM_CALLBACKS.length; i++) { ListingEditor.Callbacks.SUBMIT_FORM_CALLBACKS[i](listing, mode); }			var text = listingToStr(listing); var summary = '/* ' + upperCaseFirst(listingType) + ' */ '; if (mode == MODE_ADD) { summary = updateSectionTextWithAddedListing(summary, text, listing); } else { summary = updateSectionTextWithEditedListing(summary, text, listingTemplateWikiSyntax); }			summary += $("#input-name").val; if ($(ListingEditor.Config.EDITOR_SUMMARY_SELECTOR).val !== '') { summary += ': ' + $(ListingEditor.Config.EDITOR_SUMMARY_SELECTOR).val; }			var minor = $(ListingEditor.Config.EDITOR_MINOR_EDIT_SELECTOR).is(':checked') ? true : false; saveForm(summary, minor, sectionNumber, , ); return; };

/**		 * After the listing has been converted to a string, add additional * processing required for adds (as opposed to edits), returning an * appropriate edit summary string. */		var updateSectionTextWithAddedListing = function(originalEditSummary, listingWikiText, listing) { /* +++ BEGIN IT CUSTOM LOGIC +++ */ var summary = originalEditSummary; sectionText = restoreComments(sectionText, true); summary += ListingEditor.Config.TRANSLATIONS.added; //Creo un listing commentato dello stesso tipo di quello che aggiungerò. //Se nella sezione in cui andrò a scrivere troverò questo listing commentato, lo rimpiazzerò col nuovo. var commentedListing = "\n"; var index = 0; var index1 = sectionText.indexOf('==='); var index2 = sectionText.indexOf(''); var index6 = sectionText.indexOf('=== ' + ListingEditor.Config.TRANSLATIONS.splurge + ' ==='); var index7 = sectionText.indexOf('\n' + commentedListing,'=== ' + ListingEditor.Config.TRANSLATIONS.midrange + ' ===\n') + '* ' + listingWikiText + '\n\n' + sectionText.substr(index6-splurgeOffset); } else { //I successivi elementi vengono accodati nella sottosezione "Prezzi medi" (già priva di commento) sectionText = sectionText.substr(0, index6-splurgeOffset).replace(/\n+$/,'\n') + '* ' + listingWikiText + '\n\n' + sectionText.substr(index6-splurgeOffset); }					} else { sectionText = sectionText.substr(0, index).replace(commentedListing,'') + '* ' + listingWikiText + '\n' + sectionText.substr(index); }				}			} else { var strApp = sectionText.replace(/(\r\n|\n|\r)/gm," ").trim; if (strApp.substring(strApp.length-5) === '{{-}}') { var index2 = sectionText.lastIndexOf('{{-}}'); sectionText = sectionText.substr(0, index2).replace(commentedListing,'') + '* ' + listingWikiText + '\n{{-}}'; } else { sectionText = sectionText.replace(commentedListing,'') + '\n'+ '* ' +listingWikiText; }			}			return summary; /* +++ END IT CUSTOM LOGIC +++ */ };

/**		 * After the listing has been converted to a string, add additional * processing required for edits (as opposed to adds), returning an * appropriate edit summary string. */		var updateSectionTextWithEditedListing = function(originalEditSummary, listingWikiText, listingTemplateWikiSyntax) { var summary = originalEditSummary; if ($(ListingEditor.Config.EDITOR_CLOSED_SELECTOR).is(':checked')) { listingWikiText = ''; summary += ListingEditor.Config.TRANSLATIONS.removed; var listRegex = new RegExp('\\n\\*+\\s?' + replaceSpecial(listingTemplateWikiSyntax)); sectionText = sectionText.replace(listRegex, listingTemplateWikiSyntax); } else { summary += ListingEditor.Config.TRANSLATIONS.updated; }			sectionText = sectionText.replace(listingTemplateWikiSyntax, listingWikiText); sectionText = restoreComments(sectionText, true); return summary; };

/**		 * Render a dialog that notifies the user that the listing editor changes * are being saved. */		var savingForm = function { // if a progress dialog is already open, get rid of it			if ($(SAVE_FORM_SELECTOR).length > 0) { $(SAVE_FORM_SELECTOR).dialog('destroy').remove; }			var progress = $(' ' + ListingEditor.Config.TRANSLATIONS.saving + ' '); progress.dialog({				modal: true,				height: 100,				width: 300,				title: ''			}); $(".ui-dialog-titlebar").hide; };

/**		 * Execute the logic to post listing editor changes to the server so that * they are saved. After saving the page is refreshed to show the updated * article. */		var saveForm = function(summary, minor, sectionNumber, cid, answer) { var editPayload = { action: "edit", title: mw.config.get( "wgPageName" ), section: sectionNumber, text: sectionText, summary: summary, captchaid: cid, captchaword: answer };			if (minor) { $.extend( editPayload, { minor: 'true' } ); }			api.postWithToken(				"edit",				editPayload			).done(function(data, jqXHR) {				if (data && data.edit && data.edit.result == 'Success') {					window.location.reload;				} else if (data && data.error) {					saveFailed(ListingEditor.Config.TRANSLATIONS.submitApiError + ' "' + data.error.code + '": ' + data.error.info );				} else if (data && data.edit.spamblacklist) {					saveFailed(ListingEditor.Config.TRANSLATIONS.submitBlacklistError + ': ' + data.edit.spamblacklist );				} else if (data && data.edit.captcha) {					$(SAVE_FORM_SELECTOR).dialog('destroy').remove;					captchaDialog(summary, minor, sectionNumber, data.edit.captcha.url, data.edit.captcha.id);				} else {					saveFailed(ListingEditor.Config.TRANSLATIONS.submitUnknownError);				}			}).fail(function(code, result) {				if (code === "http") {					saveFailed(ListingEditor.Config.TRANSLATIONS.submitHttpError + ': ' + result.textStatus ); } else if (code === "ok-but-empty") { saveFailed(ListingEditor.Config.TRANSLATIONS.submitEmptyError); } else { saveFailed(ListingEditor.Config.TRANSLATIONS.submitUnknownError + ': ' + code ); }			});			savingForm;		};

/**		 * If an error occurs while saving the form, remove the "saving" dialog, * restore the original listing editor form (with all user content), and * display an alert with a failure message. */		var saveFailed = function(msg) { $(SAVE_FORM_SELECTOR).dialog('destroy').remove; $(ListingEditor.Config.EDITOR_FORM_SELECTOR).dialog('open'); alert(msg); };

/**		 * If the result of an attempt to save the listing editor content is a		 * Captcha challenge then display a form to allow the user to respond to * the challenge and resubmit. */		var captchaDialog = function(summary, minor, sectionNumber, captchaImgSrc, captchaId) { // if a captcha dialog is already open, get rid of it			if ($(CAPTCHA_FORM_SELECTOR).length > 0) { $(CAPTCHA_FORM_SELECTOR).dialog('destroy').remove; }			var captcha = $(' ').text(ListingEditor.Config.TRANSLATIONS.externalLinks); var image = $('') .attr('src', captchaImgSrc) .appendTo(captcha); var label = $('').text(ListingEditor.Config.TRANSLATIONS.enterCaptcha).appendTo(captcha); var input = $('').appendTo(captcha); captcha.dialog({				modal: true,				title: ListingEditor.Config.TRANSLATIONS.enterCaptcha,				buttons: [					{						text: ListingEditor.Config.TRANSLATIONS.submit, click: function {							saveForm(summary, minor, sectionNumber, captchaId, $('#input-captcha').val);							$(this).dialog('destroy').remove;						}					},					{						text: ListingEditor.Config.TRANSLATIONS.cancel, click: function {							$(this).dialog('destroy').remove;						}					}				]			}); };

/**		 * Convert the listing map back to a wiki text string. */		var listingToStr = function(listing) { var listingType = listing[ListingEditor.Config.LISTING_TYPE_PARAMETER]; var listingParameters = getListingInfo(listingType); var saveStr = '{{' + listingType; if (!inlineListing && listingParameters[ListingEditor.Config.LISTING_TYPE_PARAMETER].newline) { saveStr += '\n'; }			for (var parameter in listingParameters) { if (parameter === ListingEditor.Config.LISTING_TYPE_PARAMETER) { // already processed above continue; }				if (parameter === ListingEditor.Config.LISTING_CONTENT_PARAMETER) { // processed last continue; }				if ((listingParameters[parameter].skipIfEmpty || inlineListing) && listing[parameter] === '') { continue; }				saveStr += '| ' + parameter + '=' + listing[parameter]; saveStr += (inlineListing || !listingParameters[parameter].newline) ? ' ' : '\n'; }			if (ListingEditor.Config.ALLOW_UNRECOGNIZED_PARAMETERS) { // append any unexpected values for (var key in listing) { if (listingParameters[key]) { // this is a known field continue; }					if (listing[key] === '') { // skip unrecognized fields without values continue; }					saveStr += '| ' + key + '=' + listing[key]; saveStr += (inlineListing) ? ' ' : '\n'; }			}			saveStr += '| ' + ListingEditor.Config.LISTING_CONTENT_PARAMETER + '=' + listing[ListingEditor.Config.LISTING_CONTENT_PARAMETER]; saveStr += (inlineListing || !listingParameters[ListingEditor.Config.LISTING_CONTENT_PARAMETER].newline) ? ' ' : '\n'; saveStr += '}}'; return saveStr; };

/**		 * Determine if the specified DOM element contains only whitespace or * whitespace HTML characters. */		var isElementEmpty = function(element) { var text = $(element).text; if (!text.trim) { return true; }			return (text.trim == ' '); };

/**		 * Called on DOM ready, this method initializes the listing editor and * adds the "add/edit listing" links to sections and existing listings. */		var initListingEditor = function { if (!listingEditorAllowedForCurrentPage) { return; }			wrapContent; addListingButtons; addEditButtons; };

// expose public members return { MODE_ADD: MODE_ADD, MODE_EDIT: MODE_EDIT, init: initListingEditor };	};

$(document).ready(function {		ListingEditor.Core.init;	});

} ( mediaWiki, jQuery ) );

//