Nota: Después de publicar, quizás necesite actualizar la caché de su navegador para ver los cambios.

  • Firefox/Safari: Mantenga presionada la tecla Shift mientras pulsa el botón Actualizar, o presiona Ctrl+F5 o Ctrl+R (⌘+R en Mac)
  • Google Chrome: presione Ctrl+Shift+R (⌘+Shift+R en Mac)
  • Edge: mantenga presionada Ctrl mientras pulsa Actualizar, o presione Ctrl+F5
//<nowiki> /** Listing Editor v2.6.2-es 	2021-06-08  	This script is called by [[MediaWiki:InitListingTools.js]] 	Original authors: 	- ausgehe 	- torty3 	Additional contributors: 	- Andyrom75 	- Wrh2 	- RolandUnger 	Documentation and version history: 	- https://de.wikivoyage.org/wiki/Wikivoyage:ListingEditor.js */  /** CUSTOMIZATION INSTRUCTIONS:  	Modules: Config, Callbacks, Sister, Core  	Different Wikivoyage language versions have different implementations of 	the listing template, so this module must be customized for each.  The 	Config and Callbacks modules should be the ONLY code that requires 	customization - 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 wvListingEditor = ( function( mw, $ ) { 	'use strict';  // ---------------------------------- Config ----------------------------------  	/** 		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. 	*/  	var Config = function() {  		var SYSTEM = { 			version: '2.6.2-es',  			Commons: '//commons.wikimedia.org', 			Wikidata: '//www.wikidata.org', 			addSearchLang: [ 'en', 'fr' ], // for Wikidata search 			geomap: '//wikivoyage.toolforge.org/w/geomap.php',  			wikiLang: mw.config.get( 'wgPageContentLanguage' ), 			localLang: '', // this and the following ones are filled by script 			searchLang: [] 		};  		// -------------------------------------------------------------------- 		// CONFIGURE THE FOLLOWING TO MATCH THE LISTING TEMPLATE PARAMS & OUTPUT 		// --------------------------------------------------------------------  		// names of the listing templates 		var TEMPLATES = [ 'listado', 'vCard', 'ver', 'hacer', 'comprar', 'comer', 'evento', 'beber', 'dormir' ]; 		var MARKERS = [ 'marker' ];  		// -------------------------------------------------------------------- 		// TRANSLATE THE FOLLOWING BASED ON THE WIKIVOYAGE LANGUAGE IN USE 		// --------------------------------------------------------------------  		// map section heading ID to the listing template to use for that section 		var SECTION_TO_DEFAULT_TYPE = { 			'Llegar': 'station', // go 			'Desplazarse': 'public transport', // go 			'Ver': 'monument', // see 			'Hacer': 'sports', // do 			'Comprar': 'shop', // buy 			'Comer': 'restaurant', // eat 			'Beber': 'bar', // drink 			'Beber_y_salir': 'bar', // drink and night life 			'Dormir': 'hotel', // sleep 			'Educación': 'education', // education 			'Empleo': 'administration', // work 			'Seguridad': 'administration', // security 			'Salud': 'health', // health 			'Oficina': 'office' // practicalities 		};  		// 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 = ['#Orte', '#Weitere_Ziele', '#St.C3.A4dte', '#Regionen', '#Inseln', '#print-districts' ];  		var STRINGS = { 			add: 'añadir entrada', 			addTitle: 'Agregar una nueva tarjeta de presentación', 			edit: 'editar', 			editTitle: 'Edición de una tarjeta de presentación existente', 			loading: 'Inicie el editor de listado…', 			ajaxInitFailure: 'Error: el editor de listado no se puede completar previamente.', 			saving: 'Guardando…', 			enterCaptcha: 'Ingrese el CAPTCHA', 			externalLinks: 'Su edición contiene nuevos enlaces externos.',  			cancel: 'Cancelar', 			cancelTitle: 'Descartar los cambios', 			cancelMessage: 'Cuando cierre el editor, se descartarán sus entradas anteriores. ¿De verdad quieres hacer esto?', 			deleteMessage: '¿Estás seguro de que deseas eliminar el enlace a Wikidata?', 			help: '?', 			helpPage: '//es.wikivoyage.org/wiki/Hilfe:Erstellen_einer_VCard', 			helpTitle: 'Ayuda para el editor de listado', 			preview: 'Previsualizar', 			previewTitle: 'Previsualizar de listado. ¡Úselo antes de guardar!', 			previewOff: 'Sin previsualizar', 			previewOffTitle: 'Desactivar la previsualizar', 			refresh: '↺',  //  \ue031 not yet working 			refreshTitle: 'Actualizar la previsualizar', 			submit: 'Publicar', 			submitTitle: 'Guardar cambios', 			// license text should match MediaWiki:Wikimedia-copyrightwarning 			licenseText: 'Al guardar los cambios, aceptas los <a class="external" target="_blank" href="https://foundation.wikimedia.org/wiki/Special:MyLanguage/Terms_of_Use/es"> Términos de uso </a>, y aceptas irrevocablemente publicar tus contribuciones bajo la <a class="external" target="_blank" href="https://creativecommons.org/licenses/by-sa/3.0/deed.es"><i>Creative-Commons BY-SA 3.0</i>  y la <a class="external" target="_blank" href="https://en.wikipedia.org/wiki/Wikipedia:Text_of_the_GNU_Free_Documentation_License"> GFDL</a>. Aceptas que un hipervínculo o URL es atribución suficiente bajo la licencia <em>Creative Commons</em>.',  			ifNecessary: '(solo si es necesario)', 			severalGroups: '(recomendado: diferentes grupos)', 			searchOnMap: 'Buscar en un mapa', 			deleteWikidataId: 'eleminar', 			deleteWikidataIdTitle: 'Elimina la entrada de Wikidata de la lista', 			fillFromWikidata: 'Complete la entradas en Wikidata',  			validationCategory: 'Introduzca un nombre de categoría válido sin prefijo.', 			validationCoord: 'Corrija la latitud y la longitud incorrectas.', 			validationEmail: 'Corrija la(s) dirección(es) de correo electrónico incorrecta(s).', 			validationEmptyListing: 'Ingrese un nombre o una dirección.', 			validationFacebook: 'Corrija la URL o el ID de perfil de Facebook incorrectos. ', 			validationFax: 'Corrija los números de fax incorrectos.', 			validationFlickr: 'Corrija la URL o el ID de usuario de Flickr incorrectos.', 			validationImage: 'Introduzca un nombre de archivo de imagen válido sin prefijo.', 			validationInstagram: 'Corrija el nombre de usuario o la URL de Instagram incorrectos.', 			validationLastEdit: 'Corrija la fecha incorrecta del último procesamiento.', 			validationMapGroup: 'Corrija el nombre incorrecto del grupo de mapas.', 			validationMissingCoord: 'Ingrese tanto el latitud como el longitud.', 			validationMobile: 'Corrija los números de teléfono móvil incorrectos.', 			validationName: 'Corrija el nombre incorrecto o el vínculo del artículo.', 			validationNames: 'Se eliminaron los identificadores duplicados de nombres, direcciones y / o direcciones.', 			validationPhone: 'Corrija los números de teléfono incorrectos.', 			validationSkype: 'Corrija el nombre de usuario de Skype incorrecto.', 			validationTollfree: 'Corrija los números de teléfono gratuitos incorrectos.', 			validationTwitter: 'Corrija el nombre de usuario o la URL de Twitter incorrectos.', 			validationType: 'Especifique un tipo.', 			validationUrl: 'Corrija la dirección de Internet incorrecta.', 			validationYoutube: 'Corrija la URL o el ID de canal de YouTube incorrectos.', 			validationZoom: 'Corrija el nivel de zoom incorrecto (0–19).',  			commonscat: '[Cc]ategoría', 			image: '[Aa]rchivo|[Ii]magen', //Local prefix for Image (or File) 			added: 'Listado añadido para $1', 			updated: 'Listado modificado para $1', 			removed: 'Listado eliminado para $1',  			submitApiError: 'Error: el servidor devolvió un mensaje de error al intentar guardar la listado. Inténtalo de nuevo.', 			submitBlacklistError: 'Error: se encontró un elemento en la lista negra. Elimine la entrada y vuelva a intentarlo.', 			submitUnknownError: 'Error: se produjo un error desconocido al intentar guardar la listado. Inténtalo de nuevo.', 			submitHttpError: 'Error: el servidor devolvió un error HTTP al intentar guardar la listado. Inténtalo de nuevo.', 			submitEmptyError: 'Error: el servidor devolvió una respuesta vacía al intentar guardar la listado. Inténtalo de nuevo.',  			viewCommonsPageTitle: 'Vista de imagen en Wikimedia Commons ', 			viewCommonscatPageTitle: 'Enlace a la categoría de imagen en Wikimedia Commons', 			viewWikidataPage: 'Vista del registro de Wikidata', 			wikidataShared: 'Los siguientes datos se encontraron en el conjunto de datos de Wikidata. ¿Deben adoptarse los valores encontrados?', 			wikidataSharedNotFound: 'No se encontraron datos en la base de datos de Wikidata.',  			natlCurrencyTitle: 'Insertar símbolo de moneda local', 			intlCurrencyTitle: 'Insertar símbolo de moneda internacional', 			callingCodeTitle: 'Insertar código de área', 			specialCharsTitle: 'Insertar caracteres especiales', 			linkTitle: 'Enlace de Internet', 			linkText: '<img src="//upload.wikimedia.org/wikipedia/commons/thumb/2/29/OOjs_UI_icon_link-ltr-progressive.svg/16px-OOjs_UI_icon_link-ltr-progressive.svg.png" height="16" width="16" />', 			contentLimit: 1000, 			contentStatus: 'Número de caracteres: $1', 			additionalSubtypes: 'Otras características de Wikidata', 			unknownSubtypes: 'No se encontraron características conocidas.',  			deleteListingLabel: '¿Eliminar esta listado?', 			deleteListingTitle: 'Seleccione esta opción si esta institución ya no existe o si la listado debe eliminarse por cualquier otro motivo. Luego, la listado se eliminará del artículo.', 			minorEditLabel: 'Solo se cambiaron pequeñas cosas', 			minorEditTitle: 'Seleccione esta opción si solo se han realizado cambios menores, como errores ortográficos.', 			statusLabel: 'Estado', 			statusTitle: 'Información sobre el estado del artículo, como eliminación o actualización', 			summaryLabel: 'Resumen', 			summaryTitle: 'Breve resumen del motivo del cambio', 			summaryPlaceholder: 'Razón del cambio de listado', 			updateLastedit: '¿Debería establecerse la fecha del último cambio en la fecha de hoy?', 			updateTodayLabel: 'Establecer el último cambio para hoy', 			updateTodayTitle: 'Marque esta opción si la información se ha verificado como actual y sin errores. La entrada o la fecha de hoy se utiliza como fecha de actualización.',  			textPreviewLabel: 'Previsualización', 			textPreviewTitle: 'Vista previa de la listado con los datos del formulario actual', 			syntaxPreviewLabel: 'Sintaxis Wiki', 			syntaxPreviewTitle: 'Sintaxis wiki de la listado con los datos del formulario actual', 			chosenNoResults: 'No coincide con',  			yes: [ 'y', 'yes', 's', 'si', 'sí', 'true' ], 			no: [ 'n', 'no', 'false' ], 			decimalPoint: ',',  			from: 'desde %s', 			fromTo: '%s–%s', 			to: 'hasta %s',  			sep: ',|;| and | or | y | o | u ', 			skypeSep: ';| and | or | y | o | u ' 		};  		var COORD_LETTERS =  { 			N: { factor:  1, dir: 'lat' }, 			S: { factor: -1, dir: 'lat' }, 			E: { factor:  1, dir: 'long' }, 			W: { factor: -1, dir: 'long' }, 			O: { factor:  1, dir: 'long' } // German Ost = East 		};  		var CHARS = { 			intlCurrencies: [ '€', '$', '£', '¥', '₩', '&amp;nbsp;' ], 			specialChars: [ 'Ñ', 'á', 'é', 'í', 'ñ', 'ó', 'ú', 'ü', '¡', '¿', '“', '’', '–', '—', '…', '·', '&amp;nbsp;' ], 			spaceBeforeCurrencies: true, 			spaceAfterCallingCodes: true 		};  		// -------------------------------------------------------------------- 		// CONFIGURE THE FOLLOWING BASED ON WIKIVOYAGE COMMUNITY PREFERENCES 		// --------------------------------------------------------------------  		var OPTIONS = { 			// in pixels, otherwise available space 			MaxDialogWidth: 1200, 			/** set the following flag to false if the listing editor should strip away any 				listing template parameters that are not explicitly configured in the 				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. */ 			AllowUnrecognizedParameters: true, 			// write empty parameters to listing template text 			keepItDefault: false, 			inlineFormat: true,  			CopyToAliases: true, 			CopyToTypeAliases: true,  			withoutPunctuation: [ 'address', 'address-local', 'alt', 'checkin', 'checkout', 'comment', 'hours', 'name-extra', 'payment', 'price' ] 		};  		/** 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 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 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 <div> 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. 		-	keepIt: 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. 			Default keepIt = false 		-	newline: Append a newline after the parameter in the listing 			template syntax when the article is saved. 		-	aliases: aliases for parameter names. 		-	ph: placeholder. 		-	cl: tag class(es). 		-	tp: input type (select, textarea, default: input). 		-	multiple: multiple select fields.  		The following list defines the parameter succession of form output. 		Please translate only the title and the placeholder string ph. */  		var PARAMETERS = { 			name: { aliases: [ 'nombre' ], label: 'Nombre', title: 'Nombre de la institución', ph: '  Nombre de la institución' }, 			'name-extra': { label: 'Sufijo de nombre', title: 'Adición al nombre de la instalación', ph: '  Adición al nombre de la institución' }, 			'name-local': { label: 'Nombre en idioma local', title: 'Nombre de la institución en el idioma local. Además del nombre', ph: '  Ejemplo: المتحف المصري', cl: 'editor-foreign addLocalLang' }, 			'name-latin': { label: 'Nombre en romanización', title: 'Designación de la institución en la transcripción del nombre en el idioma local', ph: '  Ejemplo: al-Matḥaf al-Miṣrī' }, 			alt: { label: 'Nombre alternativo', title: 'Nombre actual alternativo de la instalación', ph: '  También conocido como' }, 			comment: { aliases: [ 'comentario' ], label: 'Comentario', title: 'Notas sobre el nombre o institución que no son o ya no forman parte del nombre. Por ejemplo, nombres anteriores.', ph: '  Nota sobre el nombre' },  			type: { aliases: [ 'tipo' ], label: 'Tipo', title: 'Tipo de la institución"', ph: 'Seleccione uno o más tipos…', tp: 'select', multiple: true, keepIt: true }, 			group: { aliases: [ 'grupo' ], label: 'Grupo de tipo', title: 'Nur zum Überschreiben benutzen! Überschreibt die automatisch ermittelte Gruppenzugehörigkeit z. B. mit buy (Einkaufen), do (Aktivitäten), drink (Ausgehen), eat (Küche), go (Anreise), see (Sehenswürdigkeiten) und sleep (Unterkunft).', ph: 'Selección de un grupo…', tp: 'select', cl: 'addGroupHint' }, 			wikidata: { label: 'Wikidata', title: 'Nombre del objeto de datos de Wikidata', ph: '  Nombre de Wikidata' }, 			auto: { label: 'Automático', title: 'Recuperación automática de toda la información de Wikidata', ph: 'Uso de Wikidata…', tp: 'select', 				text: '<option value=""></option>' + 					'<option value="y">Sí</option>' + 					'<option value="n">No (defecto)</option>' },  			url: { label: 'Sitio web', title: 'Dirección web de la institución', ph: '  Ejemplo: https://www.ejemplo.es/', cl: 'addLink' }, 			address: { aliases: [ 'dirección' ], label: 'Dirección', title: 'Dirección de la institución con calle, código postal y ciudad', ph: '  Dirección de la institución' }, 			'address-local': { label: 'Dirección en idioma local', title: 'Dirección de la institución en el idioma local. Además de la dirección', ph: '  Ejemplo: ميدان التحرير', cl: 'editor-foreign' }, 			directions: { aliases: [ 'indicaciones' ], label: 'Indicaciones', title: 'Direcciones y ubicación de la institución', ph: '  Información sobre la ubicación de la institución' }, 			'directions-local': { label: 'Direcciones en idioma local', title: 'Direcciones y ubicación de la instalación en el idioma local', ph: '  Ejemplo: بوسط البلد', cl: 'editor-foreign' }, 			lat: { label: 'Latitud', title: 'Latitud de la ubicación de la institución', ph: '  Ejemplo: 11.11111' }, 			long: { label: 'Longitud', title: 'Longitud de la ubicación de la institución', ph: '  Ejemplo: 111.11111', cl: 'addMaplink' },  			phone: { aliases: [ 'tlf' ], label: 'Teléfono', title: 'Números de teléfono de la institución, separados por comas', ph: '  Ejemplo: +55 555 555-5555', cl: 'addCC addLocalCC' }, 			mobile: { label: 'Teléfono móvil', title: 'Números de teléfono móvil de la institución, separados por comas', ph: '  Ejemplo: +55 123 555-555', cl: 'addCC' }, 			tollfree: { aliases: [ 'tlf_gratuito' ], label: 'Gratuito', title: 'Números gratuitos de teléfono de la institución, separados por comas', ph: '  Ejemplo: +49 800 100-1000', cl: 'addCC' }, 			fax: { label: 'Fax', title: 'Números de fax de la institución, separados por comas', ph: '  Ejemplo: +55 555 555-555', cl: 'addCC addLocalCC' }, 			email: { label: 'Correo electrónico', title: 'Direcciones de correo electrónico de la institución, separadas por comas', ph: '  Ejemplo: [email protected]' }, 			skype: { label: 'Nombre de Skype', title: 'Nombre de usuario de Skype de la institución', ph: '  Ejemplo: myskype' }, 			facebook: { label: 'Facebook URL', title: 'ID de perfil de Facebook de la institución o dirección web de Facebook', ph: '  Ejemplos: myfacebook, https://www.facebook.com/myfacebook', cl: 'addLink' }, 			flickr: { label: 'Grupo de flickr', title: 'Nombre del grupo de Flickr de la institución o dirección web de Flickr', ph: '  Ejemplo: myflickr', cl: 'addLink' }, 			instagram: { label: 'Nombre de Instagram', title: 'Nombre de usuario de Instagram de la institución o dirección web de Instagram', ph: '  Ejemplo: myinstagram', cl: 'addLink' }, 			twitter: { label: 'Twitter URL', title: 'Nombre de usuario de Twitter de la institución o dirección web de Twitter', ph: '  Ejemplo: mytwitter', cl: 'addLink' }, 			youtube: { label: 'Canal de Youtube', title: 'Identificador o dirección web del canal de YouTube de la institución', ph: '  Ejemplo: myyoutube', cl: 'addLink' },  			hours: { aliases: [ 'horario' ], label: 'Horario', title: 'Horarios de apertura de la institución', ph: '  Ejemplo: Lunes a viernes de 9 a. M. a 6 p. M.' }, 			checkin: { aliases: [ 'hora_entrada' ], label: 'Hora entrada', title: 'Hora de entrada más temprana (solo para alojamientos)', ph: '  Hora de entrada más temprana (solo para alojamientos)' }, 			checkout: { aliases: [ 'hora_salida' ], label: 'Hora salida', title: 'Última hora de salida (solo para alojamientos)', ph: '  Última hora de salida (solo para alojamientos)' }, 			price: { aliases: [ 'precio' ], label: 'Precio', title: 'Tarifa de entrada, tarifa de servicio o tarifa de alojamiento de la institución', ph: '  Tarifa de entrada, tarifa de servicio o tarifa de alojamiento', cl: 'addCurrencies' }, 			payment: { label: 'Métodos de pago', title: 'Métodos de pago aceptados por la institución', ph: '  Ejemplo: Master, Visa, Amex' }, 			subtype: { aliases: [ 'estrellas' ], label: 'Caracteristicas', title: 'Caracteristicas de la institución', ph: 'Seleccione una o más características…', tp: 'select', multiple: true }, 			image: { aliases: [ 'imagen' ], label: 'Imagen', title: 'Imagen que se mostrará al hacer clic en el número en el mapa', ph: '  Imagen de la institución', cl: 'addImgLink' }, 			commonscat: { label: 'Categoría de imagen', title: 'Categoría de imagen con más imágenes para esta función si no se ha configurado Wikidata', ph: '  Categoría con imágenes de la institución', cl: 'addCommonsLink' }, 			show: { label: 'Opciones de pantalla', title: 'Legt die Anzeige von Koordinatenmarkern, Koordinaten und Merkmalen der Einrichtung fest. Nur nötig, wenn die Anzeigeoptionen vom Standard (z. B. „nur Marker“) abweichen.', ph: 'Seleccione una o más opciones…', tp: 'select', multiple: true, 				text: '<optgroup label="Coordenadas" id="listing-show-coordinate">' + 					'<option value="all">Marcadores y coordenadas</option>' + 					'<option value="poi">Marcadores (defecto)</option>' + 					'<option value="coord">Coordenadas</option>' + 					'<option value="none">Sin coordenadas</option>' + 				'</optgroup>' + 				'<optgroup label="Símbolo de la institución" id="listing-show-symbol">' + 					'<option value="symbol">Símbolo MAKI</option>' + 					'<option value="noairport">Sin códigos de aeropuerto</option>' + 				'</optgroup>' + 				'<optgroup label="Caracteristicas de la institución" id="listing-show-subtypes">' + 					'<option value="nosubtype">No mostrar características</option>' + 					'<option value="nowdsubtype">Sin características de Wikidata</option>' + 				'</optgroup>' + 				'<optgroup label="Presentación del listado" id="listing-show-block">' + 					'<option value="inline">Dentro de la linea</option>' + 					'<option value="outdent">Sangría colgante</option>' + 				'</optgroup>' }, 			zoom: { label: 'Nivel del zoom', title: 'Ampliación de la sección (nivel de zoom) del mapa que se mostrará entre 0 y 19. El estándar es 17.', ph: '  Ejemplo: 17' }, 			'map-group': { label: 'Grupo de mapas', title: 'Nombre de un grupo de mapas. Esta información solo es necesaria si los marcadores se van a distribuir en diferentes mapas. Solo puede constar de los caracteres A - Z, a - z y 0–9 y debe comenzar con una letra.', ph: '  Ejemplo: GrupoDeMapas1' }, 			lastedit: { label: 'Última actualización', title: 'Fecha del último procesamiento con el formato aaaa-mm-dd, la denominada fecha ISO. Si el campo se deja en blanco, se ingresa la fecha de hoy al guardar.', hideDivIfEmpty: 'div_lastedit', ph: '2020-01-15' },  			before: { label: 'Preposición', title: 'Símbolos, partes de texto, etc. colocados al principio del listado', ph: '  Ejemplo: [[Archivo:Asterisco.jpg]]' }, 			description: { aliases: [ 'descripción', 'content' ], label: 'Descripción', title: 'Descripción breve de la institución. Un valor de referencia para la longitud del texto es de 1000 caracteres.', keepIt: true, ph: 'Descripción de la institución', tp: 'textarea' } 		};  		// ----------------------- Stop translation here -----------------------  		// Type dependent hide /show 		var HIDE_AND_SHOW = { 			'sleep': {  				hide: [], // 'div_hours'; needed for campsites etc. 				show: ['div_checkin', 'div_checkout'] 			}, 			'default':  {  				hide: ['div_checkin', 'div_checkout'], 				show: [] // 'div_hours' 			} 		};  		var REGEX = { 			name:     /^([^\[\]\|]+|\[\[[^\[\]\|]+\]\]|\[\[[^\[\]\|]+\|[^\[\]\|]+\]\])$/, 			url:      /^(https?:\/\/|\/\/)(\d{1,3}(\.\d{1,3}){0,3}|([^.\/:;<=>?\\@|\s\x00-\x2C\x7F]+\.)+[^.\/:;<=>?\\@|\d\s\x00-\x2C\x7F]{2,10}(:\d+)?)(\/?|\/[-A-Za-z0-9_.,~%+&:;#*?!=()@\/\x80-\xFF]*)$/, 			// protocol:       (https?:\/\/|\/\/) 			// domain:         (\d{1,3}(\.\d{1,3}){0,3}|([^.\/:;<=>?\\@|\s\x00-\x2C\x7F]+\.)+[^.\/:;<=>?\\@|\d\s\x00-\x2C\x7F]{2,10}(:\d+)?) 			// residual:       (\/?|\/[-A-Za-z0-9_.,~%+&:;#*?!=()@\/\x80-\xFF]*) 			// not considered: logins like login:password@, IPv6 addresses; will be added if necessary  			phone:    /^(\+[1-9]|[\d\(])([\dA-Z \-\(\)\.]+[\dA-Z ])(( ([Ee][Xx][Tt]\.? |[Aa][Pp][Pp]\.? |x)\d+)?)( *\([^\)]+\))?$/, 			email:    /^[^@^\(^\)\s]+@[^@^\(^\)\s]+\.[^@^\(^\)\s]+( *\([^\)]+\))?$/, 			skype:    /^[a-z][a-z0-9\.,\-_]{5,31}(\?(add|call|chat|sendfile|userinfo|voicemail))?( *\([^\)]+\))?$/, 			facebook: /^(https:\/\/www\.facebook\.com\/.+|(?!.*\.(?:com|net))[a-z\d.]{5,}|[-.\w\d]+\-\d+)$/i, 			flickr:   /^(https:\/\/www\.flickr\.com\/.+|\d{5,11}@N\d{2})$/, 			instagram:/^(https:\/\/www\.instagram\.com\/.+|explore\/locations\/[1-9]\d{0,15}|[0-9a-z_][0-9a-z._]{0,28}[0-9a-z_])$/, 			twitter:  /^(https:\/\/twitter\.com\/.+|[0-9a-z_]{1,15})$/i, 			youtube:  /^(https:\/\/www\.youtube\.com\/.+|UC[-_0-9A-Za-z]{21}[AQgw])$/,  			image:    new RegExp( '^(?!([Ff]ile|[Ii]mage|' + STRINGS.image + '):)' + '.+\.(tif|tiff|gif|png|jpg|jpeg|jpe|webp|xcf|ogg|ogv|svg|pdf|stl|djvu|webm|mpg|mpeg)$', 'i' ), 			commonscat: new RegExp( '^(?!(category|' + STRINGS.commonscat + '):)' + '.+$', 'i' ), 			zoom:     /^1?[0-9]$/, 			mapgroup: /^[A-Za-z][A-Za-z0-9]*$/, 			lastedit: /^((20\d{2}-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01]))|((0?[1-9]|[12][0-9]|3[01])\.(0?[1-9]|1[012])\.20\d{2}))$/ 		};  		var FIELDS = { 			name:        { regex: REGEX.name, m: STRINGS.validationName, wd: false }, 			url:         { regex: REGEX.url, m: STRINGS.validationUrl, wd: true }, 			phone:       { regex: REGEX.phone, m: STRINGS.validationPhone, wd: true, sep: STRINGS.sep }, 			mobile:      { regex: REGEX.phone, m: STRINGS.validationMobile, wd: false, sep: STRINGS.sep }, 			tollfree:    { regex: REGEX.phone, m: STRINGS.validationTollfree, wd: false, sep: STRINGS.sep }, 			fax:         { regex: REGEX.phone, m: STRINGS.validationFax, wd: true, sep: STRINGS.sep }, 			email:       { regex: REGEX.email, m: STRINGS.validationEmail, wd: true, sep: STRINGS.sep }, 			skype:       { regex: REGEX.skype, m: STRINGS.validationSkype, wd: true, sep: STRINGS.skypeSep }, 			facebook:    { regex: REGEX.facebook, m: STRINGS.validationFacebook, wd: true }, 			flickr:      { regex: REGEX.flickr, m: STRINGS.validationFlickr, wd: true }, 			instagram:   { regex: REGEX.instagram, m: STRINGS.validationInstagram, wd: true }, 			twitter:     { regex: REGEX.twitter, m: STRINGS.validationTwitter, wd: true }, 			youtube:     { regex: REGEX.youtube, m: STRINGS.validationYoutube, wd: true }, 			image:       { regex: REGEX.image, m: STRINGS.validationImage, wd: true }, 			commonscat:  { regex: REGEX.commonscat, m: STRINGS.validationCategory, wd: false }, 			zoom:        { regex: REGEX.zoom, m: STRINGS.validationZoom, wd: false }, 			'map-group': { regex: REGEX.mapgroup, m: STRINGS.validationMapGroup, wd: false }, 			lastedit:    { regex: REGEX.lastedit, m: STRINGS.validationLastEdit, wd: false } 		};  		var WIKIDATA_CLAIMS = { 			name:        { type: 'label', which: 'wiki' }, 			'name-local':{ type: 'label', which: 'local' }, 			url:         { p:  'P856' }, 			address:     { p: 'P6375', type: 'monolingual', which: 'wiki', max: 10 }, 			'address-local': { p: 'P6375', type: 'monolingual', which: 'local', max: 10 }, 			directions:  { p: 'P2795', type: 'monolingual', which: 'wiki', max: 10 }, 			'directions-local': { p: 'P2795', type: 'monolingual', which: 'local', max: 10 }, 			lat:         { p:  'P625', type: 'coordinate', which: 'latitude' }, 			long:        { p:  'P625', type: 'coordinate', which: 'longitude' },  			phone:       { p: 'P1329', max: 5, type: 'contact' }, 			fax:         { p: 'P2900', max: 3, type: 'contact' }, 			email:       { p:  'P968', type: 'email', max: 5 }, 			skype:       { p: 'P2893' }, 			facebook:    { p: 'P2013' }, 			flickr:      { p: 'P3267' }, 			instagram:   { p: 'P2003' }, 			twitter:     { p: 'P2002' }, 			youtube:     { p: 'P2397' },  			// type:     {} 			subtype:     { p: [ 'P912', 'P2012', 'P2846', 'P2848', 'P5023' ], label: 'Features', type: 'subtype', table: '', result: 'table', max: 50 }, 			hours:       { p: 'P3025', type: 'hours', max: 5 }, 			checkin:     { p: 'P8745', type: 'id' }, 			checkout:    { p: 'P8746', type: 'id' }, 			price:       { p: 'P2555', type: 'au', max: 5 }, 			payment:     { p: 'P2851', type: 'id' }, 			image:       { p:   'P18' }, 			commonscat:  { p:  'P373' } 		};  		var SHOW_OPTIONS = { // strings should be not empty 			none: '_', 			poi: '_', 			coord: '_', 			all: '_', 			symbol: '_', 			nowdsubtype: '_', 			noairport: '_', 			nosubtype: '_', 			outdent: '_', 			inline: '_' 		};  		var PROPERTIES = { 			quantity: 'P1114', 			minimumAge: 'P2899', 			maximumAge: 'P4135', 			dayOpen: 'P3027', 			dayClosed: 'P3028', 			hourOpen: 'P8626', 			hourClosed: 'P8627', 		};  		var COMMENTS = { 			contact: [ 'P366', 'P518', 'P642', 'P1001', 'P1559', 'P106' ], 			fee:     [ 'P5314', 'P518', 'P6001', 'P1264', 'P585', 'P2899', 'P4135', 'P642'], 			hours:   [ 'P8626', 'P8627', 'P3027', 'P3028' ] 		};  		// -------------------------------------------------------------------- 		// CONFIGURE THE FOLLOWING TO IMPLEMENT THE UI FOR THE LISTING EDITOR 		// --------------------------------------------------------------------  		var SELECTORS = { 			/** 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. */ 			editorDelete: '#checkbox-delete', 			editorForm: '#listingeditor-form', 			editorLastedit: '#checkbox-lastedit', 			editorMinorEdit: '#checkbox-minor', 			editorSummary: '#input-summary', 			wikidataLabel: '#input-wikidata-label',  			// document selectors 			geoIndicator: '#mw-indicator-i3-geo .wv-coord-indicator', 			// selector that identifies the listing elements into which the 			// 'edit' link will be placed 			listingEditLink: 'span.listing-metadata-items' 		};  		// -------------------------------------------------------------------- 		// ADDING OPTIONS 		// --------------------------------------------------------------------  		var LINK_FORMATTERS = { 			facebook: 'https://www.facebook.com/$1', 			flickr: 'https://www.flickr.com/photos/$1', 			instagram: 'https://www.instagram.com/$1/', 			twitter: 'https://twitter.com/$1', 			youtube: 'https://www.youtube.com/channel/$1', 			url: '$1' 		};  		var CHOSEN_OPTIONS = { 			no_results_text: STRINGS.chosenNoResults, 			width: '100%', 			rtl: false, 			allow_single_deselect: true, 			disable_search_threshold: 5 		};  		// utilities for form elements creation 		var getInputId = function( id ) { 			return 'input-' + id; 		};  		var input = function( id, add ) { 			if ( !id || id === '' ) return '';  			var el, tagId = getInputId( id ), 				p = PARAMETERS[ id === 'wikidata-label' ? 'wikidata' : id ], 				attr = mw.format( 'id="$1"', tagId ) + 					( p.cl ? mw.format( ' class="$1"', p.cl ) : '' );  			switch ( p.tp || '' ) { 				case 'select': 					if ( !p.text && !p.multiple ) 						p.text = '<option value=""></option>'; 					attr += ( p.multiple ? ' multiple="multiple"' : '' ) + 						( p.ph ? mw.format( ' data-placeholder="$1"', p.ph ) : '' ); 					el = mw.format( '<select class="chosen-select" title="$1" $2>$3</select>', p.title, attr, p.text || '' ); 					break; 				case 'textarea': 					el = mw.format( '<textarea rows="8" title="$1" $2></textarea>', p.title, attr ); 					break; 				default: 					el = mw.format( '<input type="text" title="$1" $2>', p.title, attr ); 			}  			return mw.format( '<div id="div_$1" class="editor-row">', id ) + 				mw.format( '<div><label id="label-$1" for="$2" title="$3">$4</label></div>', 					id, tagId, p.title, p.label ) + 				mw.format( '<div class="editor-input">$1</div>', el + ( add || '' ) ) + 			'</div>'; 		};  		var inputs = function( arr ) { 			var s = ''; 			for ( var id of arr ) 				s += input( id ); 			return s; 		};  		/** 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 TEMPLATES parameter arrays since that 			array provides the mapping between the editor HTML and the listing 			template fields. */  		var EDITOR_FORM_HTML = '<form id="listingeditor-form">' + 			'<div class="listingeditor-container">' + 			'<div class="listingeditor-col">' + 				// maybe a Callbacks.initFindOnMapLink or Callbacks.initSymbolFormFields update necessary 				inputs( [ 'name', 'name-extra', 'alt', 'comment', 'url', 'address', 'directions', 'lat', 'long', 					 'phone', 'tollfree', 'mobile', 'fax', 'email', 'skype', 'facebook', 'twitter', 'flickr', 'instagram', 'youtube' ] ) + 			'</div>' +  			'<div class="listingeditor-col">' + 				inputs( [ 'type', 'group' ] ) + 				input( 'subtype', 					'<div class="input-other" id="listingeditor-additionalSubtypes" style="display: none"><a href="javascript:" title="' + STRINGS.additionalSubtypes + '">[ + ]</a></div>' ) + 				input( 'show' ) + 				input( 'wikidata-label', 					'<div class="input-other" id="wikidata-tools">' + 						'<input type="hidden" id="input-wikidata"><span id="wikidata-value-link"></span> | ' + 						'<a href="javascript:" id="wikidata-remove" title="' + STRINGS.deleteWikidataIdTitle + '">' + STRINGS.deleteWikidataId + '</a>' + 					'</div>' ) + 				input( 'auto', '<div id="div_wikidata_update" class="input-other"><a href="javascript:" id="wikidata-shared">' + STRINGS.fillFromWikidata + '</a></div>' ) + 				inputs( [ 'hours', 'checkin', 'checkout', 'price', 'payment', 'image', 'commonscat', 'zoom', 					'map-group', 'before', 'name-local', 'name-latin', 'address-local', 'directions-local' ] ) + 			'</div>' + 			'</div>' + 			 			input( 'description' ) +  			// update the Callbacks.hideEditOnlyFields method if 			// the status and/or summary rows are removed or modified 			'<div id="div_status" class="editor-row">' + 				mw.format( '<div title="$1">$2</div>', STRINGS.statusTitle, STRINGS.statusLabel ) + 				'<div>' + 					// update the Callbacks.updateLastEditDate 					// method if the last edit input is removed or modified 					'<span id="div_lastedit">' + 						mw.format( '<label for="$1" title="$2">$3</label> ', getInputId( 'lastedit' ), PARAMETERS.lastedit.title, PARAMETERS.lastedit.label ) + 						'<input type="text" size="10" id="' + getInputId( 'lastedit' ) + '">' + 					'</span>' + 					'<span id="span-lasteditToday">' + 						'<input type="checkbox" id="checkbox-lastedit" />' + 						mw.format( '<label for="checkbox-lastedit" class="listingeditor-tooltip" title="$1">$2</label>', STRINGS.updateTodayTitle, STRINGS.updateTodayLabel ) + 					'</span>' + 					'<span id="span-delete">' + 						'<input type="checkbox" id="checkbox-delete">' + 						mw.format( '<label for="checkbox-delete" class="listingeditor-tooltip" title="$1">$2</label>', STRINGS.deleteListingTitle, STRINGS.deleteListingLabel ) + 					'</span>' + 				'</div>' + 			'</div>' +  			'<div id="div_summary">'+ 				'<div class="listingeditor-divider"></div>' + 				'<div class="editor-row">' + 					mw.format( '<div><label for="input-summary" title="$1">$2</label></div>', STRINGS.summaryTitle, STRINGS.summaryLabel ) + 					'<div class="editor-input">' + 						'<input type="text" id="input-summary" placeholder="' + STRINGS.summaryPlaceholder + '">' + 						mw.format( '<div id="span-minor" class="input-other"><input type="checkbox" id="checkbox-minor"><label for="checkbox-minor" class="listingeditor-tooltip" title="$1">$2</label></div>', STRINGS.minorEditTitle, STRINGS.minorEditLabel ) + 					'</div>' +						 				'</div>' + 			'</div>' +  			'<div id="listingeditor-preview" style="display: none;">' + 				'<div class="listingeditor-divider"></div>' + 				'<div class="editor-row">' + 					'<div>' + 						mw.format( '<input type="radio" name="previewSelect" id="select-preview" value="Template preview" checked="checked" /> <label for="select-preview" title="$1">$2</label><br />', STRINGS.textPreviewTitle, STRINGS.textPreviewLabel ) + 						mw.format( '<input type="radio" name="previewSelect" id="select-syntax" value="Wiki syntax" /> <label for="select-syntax" title="$1">$2</label>', STRINGS.syntaxPreviewTitle, STRINGS.syntaxPreviewLabel ) + 					'</div>' + 					'<div>' + 						'<div id="listingeditor-preview-text" class="listingeditor-preview-div"></div>' + 						'<div id="listingeditor-preview-syntax" class="listingeditor-preview-div" style="display: none"></div>' + 					'</div>' + 				'</div>' + 			'</div>' +  			'</form>';  		var MODULE_TABLES = { 			types: window.ListingEditor.types || [], 			groups: window.ListingEditor.groups || [], 			subtypes: window.ListingEditor.subtypes,  			currencies: window.ListingEditor.currencies, 			q_ids: [ window.ListingEditor.payments, window.ListingEditor.hours, window.ListingEditor.qualifiers ],  			typeList: {}, 			groupList: {}, 			subtypeList: {},  			typeAliases: {}, 			groupAliases: {}, 			subtypeAliases: {}, 		};  		// expose public members 		return { 			CHARS: CHARS, 			CHOSEN_OPTIONS: CHOSEN_OPTIONS, 			COMMENTS: COMMENTS, 			COORD_LETTERS: COORD_LETTERS, 			DISALLOW_ADD_LISTING_IF_PRESENT: DISALLOW_ADD_LISTING_IF_PRESENT, 			EDITOR_FORM_HTML: EDITOR_FORM_HTML, 			FIELDS: FIELDS, 			getInputId: getInputId, 			HIDE_AND_SHOW: HIDE_AND_SHOW, 			LINK_FORMATTERS: LINK_FORMATTERS, 			MARKERS: MARKERS, 			MODULE_TABLES: MODULE_TABLES, 			OPTIONS: OPTIONS, 			PARAMETERS: PARAMETERS, 			PROPERTIES: PROPERTIES, 			SECTION_TO_DEFAULT_TYPE: SECTION_TO_DEFAULT_TYPE, 			SELECTORS: SELECTORS, 			SHOW_OPTIONS: SHOW_OPTIONS, 			STRINGS: STRINGS, 			SYSTEM: SYSTEM, 			TEMPLATES: TEMPLATES, 			WIKIDATA_CLAIMS: WIKIDATA_CLAIMS 		}; 	}();  // -------------------------------- Callbacks ---------------------------------  	/**  		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. 	*/  	var 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 		// -------------------------------------------------------------------- 		 		var wdResults = {};  		var removeComments = function( s ) { 			return s.replace( /<!--.*?-->/g, '' ).trim(); 		};  		// character count for description 		var characterCount = function( form, mode ) { 			Core.ELEMENTS.description.keyup( function( e ) { 				var count = $( this ).val().length; 				$( '#counter-description', form ) 					.html( mw.format( Config.STRINGS.contentStatus, count ) ) 					.toggleClass( 'input-content-limit', count > Config.STRINGS.contentLimit ); 			}).trigger( 'keyup' );	 		}; 		CREATE_FORM_CALLBACKS.push( characterCount );  		/** 			Add listeners to the currency symbols, calling codes and special 			characters so that clicking on a symbol will insert it into the input. 		*/  		var initSymbolFormFields = function( form, mode ) { 			$( '.editor-charinsert', form ).click( function() { 				var _this = $( this ); 				var input = $( '#' + _this.attr( 'data-for' ) ); 				var caretPos = input[ 0 ].selectionStart; 				var oldValue = input.val(); 				var symbol = _this.find( 'a' ).text(); 				var charType = _this.attr( 'data-type' ) || ''; 				if ( Config.CHARS.spaceBeforeCurrencies && symbol != '&nbsp;' && 					charType == 'currency-char' && caretPos > 0 && 					!isNaN( oldValue.substring( caretPos-1, caretPos ) ) ) 					symbol = '&nbsp;' + symbol; 				else if ( Config.CHARS.spaceAfterCallingCodes && charType == 'phone-char' ) 					symbol = symbol + ' ';  				var newValue = oldValue.substring(0, caretPos) + symbol + oldValue.substring( caretPos ); 				input.val( newValue ).select(); 				// now setting the cursor behind the symbol inserted 				caretPos = caretPos + symbol.length; 				input[ 0 ].setSelectionRange( caretPos, caretPos ); 			}); 		}; 		CREATE_FORM_CALLBACKS.push( initSymbolFormFields );  		// handling coordinates 		var checkForSplit = function() { 			var long = Core.ELEMENTS.long; 			if ( removeComments( long.val() ) !== '' ) return;  			var lat = Core.ELEMENTS.lat; 			var value = removeComments( lat.val().toUpperCase() ); 			var coords = value.split( /[,;\|]/ ); 			if ( coords.length === 2 ) { 				lat.val( coords[ 0 ].trim() ); 				long.val( coords[ 1 ].trim() ); 				return; 			} 			for ( var d of [ 'N', 'S' ] ) { 				coords = value.split( d ); 				if ( coords.length === 2 ) { 					lat.val( coords[ 0 ].trim() + ' ' + d ); 					long.val( coords[ 1 ].trim() ); 					return; 				} 			} 		};  		var parseCoord = function( coord, aDir ) { 			var s = coord.trim(), v, l; 			var result = { coord: s, error: 2 }; // 2 = is error 			if ( s === '' ) { 				result.error = 1; 				return result; 			}  			var mx = aDir === 'lat' ? 90 : 180; 			if ( isNaN( coord ) ) { // try conversion dms -> dec 				s = s.toUpperCase() 					.replace( /[‘’′´`]/ig, "'" ) 					.replace( /''/ig, '"' ) 					.replace( /[“”″]/ig, '"' ) 					.replace( /[−–—]/ig, '-' ) 					.replace( /[_\\\/\s\0]/ig, ' ' ) 					.replace( /([A-Z])/ig, ' $1' ) 					.replace( /\s*([°"\'])/ig, '$1 ' ) 					.split( ' ' ); 				for ( var i = s.length - 1; i >= 0; i-- ) { 					s[ i ] = s[ i ].trim(); 					if ( s[ i ] === null || s[ i ] === '' ) 						s.splice( i, 1 ); 				}  				if ( s.length < 1 || s.length > 4 ) 					return result;  				var units = [ '°', "'", '"', ' ' ]; 				var res   = [ 0, 0, 0, 1 ]; // 1 = positive direction  				for ( i = 0; i < s.length; i++ ) { 					v = s[ i ].replace( units[ i ], '' ); 					if ( !isNaN( v ) ) { // a number 						v = parseFloat( v ); 						switch( i ) { 							case 3: // only for direction letter 								return result; 							case 0: 								res[ 0 ] = v; 								break; 							case 1: 							case 2: 								if ( v < 0 || v >= 60 || res[ i - 1 ] != Math.round( res[ i - 1 ] )) 									return result; 								res[ i ] = v; 						} 					} else { // not a number: allowed only at the last position 						if ( i == 0 || ( i + 1 ) != s.length || res[ 0 ] < 0 || 							v.length !== 1 || !Config.COORD_LETTERS[ v ] ) 							return result; 						l = Config.COORD_LETTERS[ v ]; 						if ( aDir !== l.dir ) 							return result; 						res[ 3 ] = l.factor; 					} 				}  				if ( res[ 0 ] < 0 ) { 					res[ 0 ] = -res[ 0 ]; 					res[ 3 ] = -1; 				} 				result.coord = ( res[ 0 ] + res[ 1 ] / 60 + res[ 2 ] / 3600 ) * res[ 3 ]; 				result.coord = Math.round( result.coord * 1E6 ) / 1E6; // only 6 digits 			} 			if ( coord < -mx || coord > mx || coord <= -180 ) 				return result;  			result.error = 0; 			return result; 		};  		var checkCoordinates = function() { 			var lat = Core.ELEMENTS.lat; 			var long = Core.ELEMENTS.long; 			var latVal = removeComments( lat.val() ); 			var longVal = removeComments( long.val() );  			var r = parseCoord( latVal, 'lat' ); 			if ( r.coord !== latVal ) 				lat.val( r.coord ); 			var result = r.error; 			lat.toggleClass( 'listingeditor-invalid-input', r.error > 1 );  			r = parseCoord( longVal, 'long' ); 			if ( r.coord !== longVal ) 				long.val( r.coord ); 			result += r.error; 			long.toggleClass( 'listingeditor-invalid-input', r.error > 1 ); 			return result; 		};  		var checkCoordInput = function(form, mode) { 			Core.ELEMENTS.long.blur(function() { 				checkCoordinates(); 			}); 			Core.ELEMENTS.lat.blur(function() { 				checkForSplit(); 				checkCoordinates(); 			}).trigger( 'blur' ); 		}; 		CREATE_FORM_CALLBACKS.push( checkCoordInput );  		/** 			Add listeners on various fields to update the "find on map" link. 		*/ 		var getValFromInput = function( sel ) { 			var el = Core.ELEMENTS[ sel ]; 			if ( el.val() === '' && el.hasClass( 'listingeditor-wikidata-placeholder' ) ) 				return el.attr( 'placeholder' ); 			else 				return removeComments( el.val() );			 		};  		var getLatlngStr = function( form ) { 			var latlngStr = '?lang=' + Config.SYSTEM.wikiLang; //			// page & location cause the geomap-link crash //			latlngStr += '&page=' + encodeURIComponent( mw.config.get( 'wgTitle' ) );  			var lat = getValFromInput( 'lat' ); 			var long = getValFromInput( 'long' ); 			if ( lat === '' || long === '' ) { 				var indicator = $( Config.SELECTORS.geoIndicator ); 				lat = indicator.attr( 'data-lat' ) || ''; 				long = indicator.attr( 'data-lon' ) || ''; 			} 			lat = parseCoord( lat, 'lat' ); 			long = parseCoord( long, 'long' ); 			if ( lat.error === 0 && long.error === 0 ) 				latlngStr += '&lat=' + lat.coord + '&lon=' + long.coord + '&zoom=15';  //			var address = getValFromInput( 'address' ); //			var name = getValFromInput( 'name' ); //			if ( address !== '' ) //				latlngStr += '&location=' + encodeURIComponent( address ); //			else if ( name !== '' ) //				latlngStr += '&location=' + encodeURIComponent( name );  			return latlngStr;	 		};  		var initFindOnMapLink = function( form, mode ) { 			$( '.addMaplink', form ).parent() 				.append( $( '<div class="input-other"><a id="geomap-link" target="_blank">' + Config.STRINGS.searchOnMap + '</a></div>' ) );  			var geolink = $( '#geomap-link', form ); 			function updateGeolink() { 				geolink.attr( 'href', Config.SYSTEM.geomap + getLatlngStr( form ) ); 			}  			if ( geolink.length ) { 				Core.ELEMENTS.address.change( updateGeolink ); 				Core.ELEMENTS.lat.change( updateGeolink ); 				Core.ELEMENTS.long.change( updateGeolink ).trigger( 'change' ); 			} 		}; 		CREATE_FORM_CALLBACKS.push( initFindOnMapLink );  		/** 			Add listeners on type selector field. 		*/ 		var typesChanged = function( values, form ) { 			var color, different = false, first = '', group, i, obj, sleep = false, val;  			// make firstType first if existent 			if ( Core.firstType !== '' ) { 				for ( i = 0; i < values.length; i++ ) { 					if ( values[ i ] == Core.firstType ) { 						values.splice( i, 1 ); 						values.unshift( Core.firstType ); 						break; 					} 					if ( i == values.length - 1 ) 						Core.firstType = ''; 				} 			}  			for ( i = 0; i < values.length; i++ ) { 				val = values[ i ]; 				for ( obj of Config.MODULE_TABLES.types ) 					if ( obj.type === val ) { 						group = obj.group; 						break; 					} 				if ( i === 0 ) 					first = group; 				else if ( group != first ) 					different = true; 				if ( group == 'sleep' ) 					sleep = true; 			} 			obj = ( sleep ? Config.HIDE_AND_SHOW.sleep : Config.HIDE_AND_SHOW[ first ] ) 				|| Config.HIDE_AND_SHOW.default; 			for( i of obj.show ) 				$( '#' + i, form ).show(); 			for( i of obj.hide ) 				if ( $( '#' + i + ' input', form ).val() === '' ) 					$( '#' + i, form ).hide();  			// set input shadow 			color = '#f0f0f0'; 			for ( obj of Config.MODULE_TABLES.groups ) 				if ( obj.group === first ) { 					color = obj.color; 					break; 				} 			obj = $( '#div_type .chosen-choices', form ); 			if ( obj.length ) 				obj.css( 'box-shadow', '20px 0 0 0 ' + color + ' inset' ); 			else { 				// chosen plugin is maybe not yet active 				var style = '#div_type .chosen-choices { box-shadow: 20px 0 0 0 ' + color + ' inset }'; 				$( 'head' ).append( '<style type="text/css">' + style + '</style>' ); 			}  			// set hint to group 			$( '.group-hint', form ).text( different ? Config.STRINGS.severalGroups : Config.STRINGS.ifNecessary ); 		};  		var initTypeSelector = function( form, mode ) { 			Core.ELEMENTS.group.parent().append( $( '<div class="input-other group-hint"></div>' ) ); 			Core.ELEMENTS.type.on( 'keydown keyup change click' , function() { 				typesChanged( $( this ).val(), form ); 			}).trigger( 'keyup' ); 		}; 		CREATE_FORM_CALLBACKS.push( initTypeSelector ); 		 		var initGroupSelector = function( form, mode ) { 			Core.ELEMENTS.group.on( 'keydown keyup change click', function() { 				var color = '#f0f0f0', obj; 				for ( obj of Config.MODULE_TABLES.groups ) 					if ( obj.group === this.value ) { 						color = obj.color; 						break; 					} 				$( '#input_group_chosen .chosen-single', form ) 					.css( 'box-shadow', '20px 0 0 0 ' + color + ' inset' ); 			}).trigger( 'keyup' ); 		}; 		CREATE_FORM_CALLBACKS.push( initGroupSelector ); 		 		var initLastEditCheckBox = function( form, mode ) { 			$( Config.SELECTORS.editorLastedit, form ).change( function() { 				if ( this.checked && $( '#div_lastedit', form ).is( ':visible' ) ) 					Core.ELEMENTS.lastedit.val( getCurrentDate() ); 			}); 		}; 		CREATE_FORM_CALLBACKS.push( initLastEditCheckBox );  		var hideEditOnlyFields = function( form, mode ) { 			if ( mode !== Core.MODE_EDIT ) { 				$( '#div_status', form ).hide(); 				$( '#div_summary', form ).hide(); 			} 		}; 		CREATE_FORM_CALLBACKS.push( hideEditOnlyFields );  		// Check against regex 		var regexTest = function( field, val ) { 			var i, s, sRegex, test = true, valTab; 			val = val.trim(); 			if ( field.sep ) { 				sRegex = new RegExp( '(' + field.sep + ')(?![^(]*\\))', 'ig' ); 				valTab = val.split( sRegex ); 				sRegex = new RegExp( '^(' + field.sep.replace( / /g , '' ) + ')$', 'ig' ); 				for ( i = valTab.length - 1; i >= 0; i-- ) { 					valTab[ i ] = valTab[ i ].trim().replace( sRegex, '' ); 					if ( valTab[ i ] === '' ) valTab.splice( i, 1 ); 				} 			} else 				valTab = [ val ]; 			for ( s of valTab ) { 				test = field.regex.test( s ); 				if ( !test ) break; 			} 			return test; 		};  		// Field checks against regex 		var initCheckAgainstRegex = function( key, field, form ) { 			var val10; 			Core.ELEMENTS[ key ].blur( function() { 				var _this = $( this, form ); 				var val = removeComments( _this.val() ); 				if ( field.wd && val !== '' && Core.checkYesNo( val ) !== '' ) 					_this.removeClass( 'listingeditor-invalid-input' ); 				else { 					val10 = val.substr( 0, 10 ); 					if ( key === 'url' && !regexTest( field, val ) 						&& val.search( 'ht' ) !== 0 && val10.search( ':' ) < 0 						&& val10.search( '//' ) < 0 ) { 						val10 = 'http://' + val; 						if ( regexTest( field, val10 ) ) { 							val = val10; 							_this.val( val ); 						} 					} 					_this.toggleClass( 'listingeditor-invalid-input', 						val !== '' && !regexTest( field, val ) ); 				} 			}).trigger( 'blur' ); 		};  		var checkFields = function( form, mode ) { 			for ( var parameter in Config.FIELDS ) 				initCheckAgainstRegex( parameter, Config.FIELDS[ parameter ], form); 		}; 		CREATE_FORM_CALLBACKS.push( checkFields );  		var setDefaultPlaceholders = function( form ) { 			for ( var parameter in Config.PARAMETERS ) { 				var obj = Config.PARAMETERS[ parameter ]; 				if ( obj.ph && ( !obj.tp || obj.tp !== 'select' ) ) 					Core.ELEMENTS[ parameter ].attr( 'placeholder', obj.ph ) 						.addClass( 'listingeditor-default-placeholder' ) 						.removeClass( 'listingeditor-wikidata-placeholder' ); 			} 			$( Config.SELECTORS.wikidataLabel ).attr( 'placeholder', Config.PARAMETERS.wikidata.ph ) 				.addClass( 'listingeditor-default-placeholder' ); 		};  		var updatePlaceholder = function( key, value ) { 			if ( value && Core.ELEMENTS[ key ] ) 				Core.ELEMENTS[ key ].attr( 'placeholder', value ) 					.addClass( 'listingeditor-wikidata-placeholder' ) 					.removeClass( 'listingeditor-default-placeholder' ) 					.trigger( 'change' ); 		};  		var updatePlaceholders = function( id, form ) { 			setDefaultPlaceholders( form );  			var success = function( jsonObj ) { 				var item, key, res; 				var addSubtypes = $( '#listingeditor-additionalSubtypes' ); 				addSubtypes.hide(); 				wdResults = {}; 				for ( key in Config.WIKIDATA_CLAIMS ) { 					item = Config.WIKIDATA_CLAIMS[ key ]; 					res = Sister.wb_getStatements( id, jsonObj, item ); 					if ( res ) 						wdResults[ key ] = res; 				} 				if ( !wdResults.address && wdResults[ 'address-local' ] ) { 					wdResults.address = wdResults[ 'address-local' ]; 					delete wdResults[ 'address-local' ]; 				} 				for ( key in wdResults ) { 					if ( key === 'subtype' ) { 						wdResults.subtype = Core.sortSubtypes( wdResults.subtype ); 						addSubtypes.show(); 						continue; 					} 					updatePlaceholder( key, wdResults[ key ] ); 					if ( key === 'name' ) 						$( Config.SELECTORS.wikidataLabel ).attr( 'placeholder', wdResults.name ) 							.addClass( 'listingeditor-default-placeholder' );				} 			}; 			Sister.wb_getEntity( id, success ); 		};  		var wikidataLookup = function( form, mode ) { 			// get the display value for the pre-existing wikidata record ID 			var wikidataRemove = function(form) { 				Core.ELEMENTS.wikidata.val(""); 				$(Config.SELECTORS.wikidataLabel, form).val(''); 				$('#input-auto').val(''); 				$('#wikidata-tools', form).hide(); 				$('#div_auto', form).hide(); 				setDefaultPlaceholders(form); 			};  			var id = removeComments( Core.ELEMENTS.wikidata.val() ); 			if ( id ) { 				wikidataLink( form, id ); 				var success = function( jsonObj ) { 					var id = Core.ELEMENTS.wikidata.val(); 					var label = Sister.wb_getLabels( id, jsonObj ) || ''; 					label = label.wiki !== '' ? label.wiki : id; 					$( Config.SELECTORS.wikidataLabel ).val( label ); 				}; 				Sister.wb_getEntity( id, success, 'labels' ); 				updatePlaceholders( id, form ); 			} else 				wikidataRemove(form); 			// set up autocomplete to search for results as the user types 			$( Config.SELECTORS.wikidataLabel, form ).autocomplete({ 				source: function( request, response ) { 					var ajaxUrl = Sister.API_WIKIDATA; 					var ajaxData = { 						action: 'wbsearchentities', 						search: request.term, 						language: Config.SYSTEM.wikiLang, 						uselang: Config.SYSTEM.wikiLang 					}; 					var ajaxSuccess = function( jsonObj ) { 						response(parseWikiDataResult(jsonObj)); 					}; 					Sister.ajaxSisterSiteSearch(ajaxUrl, ajaxData, ajaxSuccess); 				}, 				select: function(event, ui) { 					Core.ELEMENTS.wikidata.val(ui.item.id); 					wikidataLink("", ui.item.id);  					updatePlaceholders(ui.item.id, form ); 				} 			}).data("ui-autocomplete")._renderItem = function(ul, item) { 				var label = item.label + " <small>" + item.id + "</small>"; 				if ( item.description ) 					label += "<br /><small>" + item.description + "</small>"; 				return $("<li>").data('ui-autocomplete-item', item).append($("<a>").html(label)).appendTo(ul); 			}; 			// add a listener to the "remove" button so that links can be deleted 			$('#wikidata-remove', form).click(function() { 				if ( confirm( Config.STRINGS.deleteMessage ) ) 					wikidataRemove(form); 			}); 			$( Config.SELECTORS.wikidataLabel, form ).change(function() { 				if ( !$(this).val() ) 					wikidataRemove(form); 			}); 			$( '#listingeditor-additionalSubtypes a', form ).click( function() { 				var msg = '', t; 				if ( wdResults.subtype ) 					for ( t of wdResults.subtype ) { 						if ( msg !== '') 							msg += ', '; 						t = t.split( ':' ); 						t[ 1 ] = t.length > 1 ? parseInt( t[ 1 ] ) : 1; 						if ( !Config.MODULE_TABLES.subtypeList[ t[ 0 ] ] ) 							continue; 						t[ 0 ] = Config.MODULE_TABLES.subtypeList[ t[ 0 ] ].n; // translate subtypes 						if ( t[ 0 ].indexOf( '[' ) > -1 ) { 							if ( t[ 1 ] > 1 ) 								t[ 0 ] = t[ 1 ] + ' ' + t[ 0 ].replace( /\[([^\[\]]*)(\|[^\[\]]*)?\]/g, '$1' ); 							else 								t[ 0 ] = t[ 0 ].replace( /\[([^\[\]]*)\|([^\[\]]*)\]/g, '$2' ); 						} 						msg += t[ 0 ].replace( /\[([^\[\]]*)\]/g, '' ) 							.replace( /[,;\/].*$/ig, '' ); 					} 					if ( msg === '' ) 						msg = Config.STRINGS.unknownSubtypes; 					alert( Config.STRINGS.additionalSubtypes + ':\n\n' + msg ); 			}); 			$('#wikidata-shared', form).click(function() { 				updateWikidataSharedFields( form ); 			}); 			Core.ELEMENTS.image.parent().append( $( '<div id="image-value-link" class="input-other"></div>' ) ); 			Sister.initializeSisterSiteAutocomplete( { 				apiUrl: Sister.API_COMMONS, 				selector: Core.ELEMENTS.image, 				form: form, 				ajaxData: { namespace: 6 }, 				updateLinkFunction: commonsLink 			} ); 			Core.ELEMENTS.commonscat.parent().append( $( '<div id="commonscat-value-link" class="input-other"></div>' ) ); 			Sister.initializeSisterSiteAutocomplete( { 				apiUrl: Sister.API_COMMONS, 				selector: Core.ELEMENTS.commonscat, 				form: form, 				ajaxData: { namespace: 14 }, 				updateLinkFunction: commonscatLink 			} ); 		};  		var updateSiteLink = function(siteLinkData, form) { 			var input = $( siteLinkData.inputSelector, form ); 			var siteLink = $(siteLinkData.linkSelector, form); 			var val = removeComments( input.val() || '' ); 			if ( val === '' && input.hasClass( 'listingeditor-wikidata-placeholder' ) ) 				val = input.attr( 'placeholder' ); 			if ( val === '' ) 				siteLink.hide(); 			else { 				siteLinkData.href = Config.SYSTEM.Commons + '/wiki/' 					+ mw.util.wikiUrlencode(siteLinkData.namespace + val); 				var link = $("<a />", { 					target: "_new", 					href: siteLinkData.href, 					title: siteLinkData.linkTitle 				}).append( $( siteLinkData.text ) ); 				siteLink.html(link).show(); 			} 		};  		var commonsLink = function(value, form) { 			var siteLinkData = { 				inputSelector: '#input-image', 				linkSelector: '#image-value-link', 				namespace: 'File:', 				linkTitle: Config.STRINGS.viewCommonsPageTitle, 				text: Config.STRINGS.linkText 			}; 			updateSiteLink( siteLinkData, form ); 		};  		var commonscatLink = function(value, form) { 			var siteLinkData = { 				inputSelector: '#input-commonscat', 				linkSelector: '#commonscat-value-link', 				namespace: 'Category:', 				linkTitle: Config.STRINGS.viewCommonscatPageTitle, 				text: Config.STRINGS.linkText 			}; 			updateSiteLink( siteLinkData, form ); 		};  		var updateFieldIfNotNull = function( key, value ) { 			if ( value ) 				Core.ELEMENTS[ key ].val( value ); 		};  		var updateWikidataSharedFields = function( form ) { 			var key, msg = '';  			for ( key in wdResults ) 				msg += '\n' + Config.PARAMETERS[ key ].label + ': ' 					+ ( typeof wdResults[ key ] == 'object' ? wdResults[ key ].join( ', ' ) : wdResults[ key ] );  			if ( msg !== '' ) { 				if ( confirm( Config.STRINGS.wikidataShared + '\n' + msg ) ) { 					for ( key in wdResults ) { 						switch( key ) { 							case 'image': 								updateFieldIfNotNull( 'image', wdResults.image ); 								commonsLink( wdResults.image, form ); 								break; 							case 'commonscat': 								updateFieldIfNotNull( 'commonscat', wdResults.commonscat ); 								commonscatLink( wdResults.commonscat, form ); 								break; 							case 'subtype': 								if ( wdResults.subtype.length ) { 									var sel = Core.ELEMENTS.subtype, i, j; 									var old = sel.val(); 									for ( i = 0; i < old.length; i++ ) { 										for ( j = wdResults.subtype.length - 1; j >= 0; j-- ) { 											if ( wdResults.subtype[ j ] == old[ i ] ) { 												wdResults.subtype.splice( j, 1 ); 												break; 											} 										} 									} 									sel.val( old.concat( wdResults.subtype ) ) 										.trigger( 'chosen:updated' ); 								} 								break; 							default: 								updateFieldIfNotNull( key, wdResults[ key ] ); 						} 					} 					Core.ELEMENTS.auto.val( '' ).trigger( 'chosen:updated' ); 					Core.ELEMENTS.name.focus(); 				} 			} else 				alert( Config.STRINGS.wikidataSharedNotFound ); 		};  		var parseWikiDataResult = function( jsonObj ) { 			var results = []; 			for ( var result of $( jsonObj.search ) ) { 				var label = result.label; 				if ( result.match && result.match.text ) 					label = result.match.text; 				var data = { 					value: label, 					label: label, 					description: result.description, 					id: result.id 				}; 				results.push( data ); 			} 			return results; 		};  		var wikidataLink = function(form, value) { 			$( '#wikidata-value-link', form ).html( $( '<a />', { 				target: '_new', 				href: Config.SYSTEM.Wikidata + '/wiki/' + mw.util.wikiUrlencode(value), 				title: Config.STRINGS.viewWikidataPage, 				text: value 			}) ); 			Core.ELEMENTS.auto.val( 'y' ).trigger( 'chosen:updated' ); 			$( '#wikidata-value-display-container', form ).show(); 			$( '#div_auto', form ).show(); 		}; 		CREATE_FORM_CALLBACKS.push( wikidataLookup );  		var selectPreview = function(form, mode) { 			$( 'input[name=previewSelect]', form ).click( function() { 				var checked = $( '#select-preview', form ).prop( 'checked' ); 				$( '#listingeditor-preview-text', form ).toggle( checked ); 				$( '#listingeditor-preview-syntax', form ).toggle( !checked ); 			}); 		}; 		CREATE_FORM_CALLBACKS.push( selectPreview );  		var addLinks = function( form, mode ) { 			$( '.addLink', form ).each( function() { 				var _this = $( this ); 				var id = _this.attr('id').replace( 'input-', '' ); 				_this.parent().append( $( '<div class="input-other"></div>' ) 					.attr( 'id', 'link-' + id ) ); 				_this.change( function() { 					var val = removeComments( _this.val() ); 					if ( val === '' && _this.hasClass( 'listingeditor-wikidata-placeholder' ) ) 						val = _this.attr( 'placeholder' ); 					if ( val !== '' && Core.checkYesNo( val ) === '' ) { 						if ( val.indexOf( 'http' ) ) 							val = mw.format( Config.LINK_FORMATTERS[ id ], val ); 						var link = $( '<a />', { 							target: '_new', 							href: val, 							title: Config.STRINGS.linkTitle, 						}).append( $( Config.STRINGS.linkText ) ) ; 						$( '#link-' + id, form ).html( link ); 					} else 						$( '#link-' + id, form ).empty(); 					var tabables = $( "input[tabindex != '-1']:visible", form ); 					var index = tabables.index( this ); 					if ( !Core.ELEMENTS.name.is( ':focus' ) ) 						tabables.eq( index + 1 ).focus(); 				}).trigger( 'change' ); 			}); 		}; 		CREATE_FORM_CALLBACKS.push( addLinks );  		var chosenInit = function( form, mode ) { 			$( '.chosen-select', form ).chosen( Config.CHOSEN_OPTIONS ); 			Core.ELEMENTS.show.change( function() { 				var coordGroup = $( '#listing-show-coordinate option', form ); 				var isCoord = false; 				coordGroup.each( function() { 					if ( $( this ).is( ':selected' ) ) 						isCoord = true; 				}); 				if ( isCoord ) 					coordGroup.each( function() { 						if ( !$( this ).is( ':selected' ) ) 							$( this ).prop( 'disabled', true ); 					}); 				else 					coordGroup.prop( 'disabled', false );  				var showGroup = $( '#listing-show-block option', form ); 				var isShow = false; 				showGroup.each( function() { 					if ( $( this ).is( ':selected' ) ) 						isShow = true; 				}); 				if ( isShow ) 					showGroup.each( function() { 						if ( !$( this ).is( ':selected' ) ) 							$( this ).prop( 'disabled', true ); 					}); 				else 					showGroup.prop( 'disabled', false );  				$( this ).trigger( 'chosen:updated' ); 			}).trigger( 'change' ); 			Core.ELEMENTS.group.trigger( 'keyup' ); 		}; 		CREATE_FORM_CALLBACKS.push( chosenInit ); 		 		// -------------------------------------------------------------------- 		// LISTING EDITOR FORM SUBMISSION CALLBACKS 		// --------------------------------------------------------------------  		/** 			Return the current date in the format "2020-01-31". 		*/ 		var getCurrentDate = function() { 			var today = new Date(); 			var date = today.getFullYear() + '-'; 			// Date.getMonth() returns 0-11 			date += ( today.getMonth() + 1 ).toString().padStart( 2, '0' ) + '-'; 			return date + today.getDate().toString().padStart( 2, '0' ); 		};  		/** 			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 currentDate = getCurrentDate(); 			if ( $( Config.SELECTORS.editorLastedit ).is( ':checked' ) ||  				( 					mode == Core.MODE_ADD && 						( 							listing.hours + listing.checkin + listing.checkout 							+ listing.price !== '' 						) 					) 				) 				listing.lastedit = currentDate; 			else 				if ( listing.lastedit !== '' ) { 					listing.lastedit = listing.lastedit.replace( /\-(\d)\-/g, '-0$1-' ) 						.replace( /\-(\d)$/g, '-0$1' ); 					if ( listing.lastedit !== currentDate && confirm( Config.STRINGS.updateLastedit ) ) 						// with OK/Cancel buttons, Yes/No is more complex 						listing.lastedit = currentDate; 				} 		}; 		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 ) { 			var name = Core.ELEMENTS.name; 			var wikidata = Core.ELEMENTS.wikidata.val(); 			// Fill name field from Wikidata 			if ( name.val() === '' && wikidata !== '' && 				name.filter( '.listingeditor-wikidata-placeholder' ).length > 0 ) { 				name.val( name.attr( 'placeholder' ) ); 				return; 			} 			if ( name.val() === '' && Core.ELEMENTS.address.val() === '' && 				Core.ELEMENTS.alt.val() === '' && wikidata === '' )  				validationFailureMessages.push( Config.STRINGS.validationEmptyListing ); 		}; 		VALIDATE_FORM_CALLBACKS.push( validateListingHasData );  		/** 			Delete group parameter if identical to types group. 		*/ 		var isGroupNecessary = function( validationFailureMessages ) { 			var types = Core.ELEMENTS.type.val(); 			var group = Core.ELEMENTS.group; 			var wikidata = Core.ELEMENTS.wikidata.val();  			if ( types.length === 0 && group.val() === '' && wikidata === '' ) { 				validationFailureMessages.push(Config.STRINGS.validationType); 				return; 			} 			if ( types.length === 0 ) 				return;  			var different = false, first = '', i, obj; 			for ( i = 0; i < types.length; i++ ) 				for ( obj of Config.MODULE_TABLES.types ) 					if ( types[ i ] === obj.type ) { 						if ( i === 1 ) 							first = obj.group; 						if ( first !== obj.group ) 							different = true; 						break; 					} 			if ( different ) 				return; 			// if type group equals group then delete group 			if ( first === group ) 				group.val( '' ); 		}; 		VALIDATE_FORM_CALLBACKS.push( isGroupNecessary );  		/** 			Validate coordinates 		*/ 		var validateCoords = function( validationFailureMessages ) { 			var lat = removeComments( Core.ELEMENTS.lat.val() ); 			var long = removeComments( Core.ELEMENTS.long.val() ); 			if ( lat === '' && long === '' ) 				return; 			if ( lat === '' ) { 				validationFailureMessages.push( Config.STRINGS.validationMissingCoord ); 				return; 			} 			checkForSplit(); 			if ( long === '' ) { 				validationFailureMessages.push( Config.STRINGS.validationMissingCoord ); 				return; 			} 			if ( checkCoordinates() > 0 ) 				validationFailureMessages.push( Config.STRINGS.validationCoord ); 		}; 		VALIDATE_FORM_CALLBACKS.push( validateCoords ); 		 		/** 			Implement SIMPLE RegExp validation. Invalid entries can 			still get through, but this method implements a minimal amount of 			validation in order to catch the worst offenders. 		*/  		var validateFields = function( validationFailureMessages ) { 			var field, parameter, val; 			for ( parameter in Config.FIELDS ) { 				field = Config.FIELDS[ parameter ]; 				val = Core.ELEMENTS[ parameter ].val().trim(); 				if ( field.wd && val !== '' && Core.checkYesNo( val ) !== '' ) 					return; 				if ( val !== '' && !regexTest( field, val ) ) 					validationFailureMessages.push( field.m ); 			} 		}; 		VALIDATE_FORM_CALLBACKS.push( validateFields );  		// remove identical names 		var ckeckNames = function( key1, key2 ) { 			var val = removeComments( Core.ELEMENTS[ key1 ].val().toLowerCase() ); // case-insensitve check 			if ( val !== '' && val === removeComments( Core.ELEMENTS[ key2 ].val().toLowerCase() ) ) { 				Core.ELEMENTS[ key2 ].val( '' ); 				return 1; 			} 			return 0; 		};  		var checkMultipleNames = function( validationFailureMessages ) { 			var result = ckeckNames( 'name', 'name-local' ); 			result += ckeckNames( 'alt', 'comment' ); 			result += ckeckNames( 'name', 'alt' ); 			result += ckeckNames( 'name', 'comment' ); 			result += ckeckNames( 'address', 'address-local' ); 			result += ckeckNames( 'directions', 'directions-local' ); 			if ( result > 0 ) 				validationFailureMessages.push( Config.STRINGS.validationNames ); 		}; 		VALIDATE_FORM_CALLBACKS.push( checkMultipleNames );  		// expose public members 		return { 			CREATE_FORM_CALLBACKS: CREATE_FORM_CALLBACKS, 			SUBMIT_FORM_CALLBACKS: SUBMIT_FORM_CALLBACKS, 			VALIDATE_FORM_CALLBACKS: VALIDATE_FORM_CALLBACKS, 			removeComments: removeComments, 			setDefaultPlaceholders: setDefaultPlaceholders 		}; 	}();  // ---------------------------------- Sister ----------------------------------  	/** 		Sister implements functionality for information 		interchange to Wikimedia sister websites 	*/  	var Sister = function() { 		var API_WIKIDATA = Config.SYSTEM.Wikidata + '/w/api.php'; 		var API_COMMONS = Config.SYSTEM.Commons + '/w/api.php';  		var initializeSisterSiteAutocomplete = function( siteData ) { 			var sel = $( siteData.selector ); 			var currentValue = sel.val(); 			if ( currentValue ) 				siteData.updateLinkFunction( currentValue, siteData.form ); 			sel.change( function() { 				siteData.updateLinkFunction( sel.val(), siteData.form ); 			}); 			siteData.selectFunction = function(event, ui) { 				siteData.updateLinkFunction(ui.item.value, siteData.form); 			}; 			var ajaxData = siteData.ajaxData; 			ajaxData.action = 'opensearch'; 			ajaxData.list = 'search'; 			ajaxData.limit = 10; 			ajaxData.redirects = 'resolve'; 			var parseAjaxResponse = function( jsonObj ) { 				var results = [], i, title; 				var titleResults = $( jsonObj[ 1 ] ); 				for ( i = 0; i < titleResults.length; i++ ) { 					title = titleResults[ i ]; 					results.push( { 						value: title.replace( /^(File:|Category:)/i, '' ), 						label: title, 						description: $( jsonObj[ 2 ] )[ i ], 						link: $( jsonObj[ 3 ] )[ i ] 					} ); 				} 				return results; 			}; 			initializeAutocomplete( siteData, ajaxData, parseAjaxResponse ); 		};  		var initializeAutocomplete = function( siteData, ajaxData, parseAjaxResponse ) { 			var autocompleteOptions = { 				source: function( request, response ) { 					ajaxData.search = request.term; 					var ajaxSuccess = function( jsonObj ) { 						response( parseAjaxResponse( jsonObj ) ); 					}; 					ajaxSisterSiteSearch( siteData.apiUrl, ajaxData, ajaxSuccess ); 				} 			}; 			if ( siteData.selectFunction ) 				autocompleteOptions.select = siteData.selectFunction; 			siteData.selector.autocomplete( autocompleteOptions ) 				.data( 'ui-autocomplete' )._renderItem = function( ul, item ) { 					var isImage = item.label.match( /^(File:)/i ); 					var label = item.label.replace( /^(File:|Category:)/i, '' ); 					if ( isImage ) 						label = '<span class="autocomplete-thumbnail" style="background-image: url(&quot;https://commons.wikimedia.org/wiki/Special:FilePath/' 							+ label.replace( /' '/g, '_' ) + '?width=200&quot;);"></span> ' + label; 					return $( '<li>' ).data( 'ui-autocomplete-item', item ) 						.append( $( '<a>' ).html( label ) ).appendTo(ul); 				}; 		};  		// perform an ajax query of a sister site 		var ajaxSisterSiteSearch = function( url, data, success ) { 			data.format = 'json'; 			$.ajax({ 				url: url, 				data: data, 				dataType: 'jsonp', 				success: success 			}); 		};  		// Wikidata support 		var wb_getEntity = function( id, success, props ) { 			props = props || 'labels|claims|datatype'; 			var languages = [].concat( Config.SYSTEM.searchLang ); 			if ( Config.SYSTEM.localLang !== '' ) 				languages.push( Config.SYSTEM.localLang ); 			languages = languages.join( '|' ); 			var data = { 				action: 'wbgetentities', 				ids: id, 				languages: languages, 				props: props 			}; 			ajaxSisterSiteSearch( API_WIKIDATA, data, success ); 		};  		// parse the wikidata "entity" object from the wikidata response 		var wb_checkEntity = function( id, jsonObj ) { 			return jsonObj && jsonObj.entities && jsonObj.entities[ id ] ? jsonObj.entities[ id ] : null; 		};  		// parse the wikidata display label from the wikidata response 		var wb_getLabels = function( id, jsonObj ) { 			var entityObj = wb_checkEntity( id, jsonObj ); 			if ( !entityObj || !entityObj.labels ) 				return null; 			var wiki = '', local = '', lang; 			for ( lang of Config.SYSTEM.searchLang ) 				if ( entityObj.labels[ lang ] ) { 					wiki = entityObj.labels[ lang ].value; 					break; 				} 			if ( Config.SYSTEM.localLang !== '' && entityObj.labels[ Config.SYSTEM.localLang ] ) 				local = entityObj.labels[ Config.SYSTEM.localLang ].value; 			return { wiki: wiki, local: local }; 		};  		var wb_getAllStatements = function( entityClaims, property ) { 			var obj, propertyObj, statements = []; 			if ( !entityClaims || !entityClaims[ property ] ) 				return statements; 			propertyObj = entityClaims[ property ]; 			if ( !propertyObj || propertyObj.length === 0 ) 				return statements;  			for ( obj of propertyObj ) 				if ( obj.mainsnak && obj.mainsnak.snaktype === 'value' && 					obj.mainsnak.datavalue ) 					statements.push( { 						value: obj.mainsnak.datavalue.value, 						qualifiers: obj.qualifiers, //						references: obj.references, 						rank: obj.rank 					} ); 			return statements; 		};  		var wb_getBestStatements = function( entityClaims, property ) { 			var statements = []; 			var allStatements = wb_getAllStatements( entityClaims, property ); 			if ( !allStatements || allStatements.length === 0 ) 				return statements;  			var rank = 'normal', statement; 			for ( statement of allStatements ) 				if ( statement.rank === rank ) 					statements.push( { value: statement.value, qualifiers: statement.qualifiers } ); 				else if ( statement.rank === 'preferred' ) { 					rank = 'preferred'; 					// remove all previous statements 					statements = [ { value: statement.value, qualifiers: statement.qualifiers } ]; 				} 			return statements; 		};  		// get Wikidata Id label 		var wb_getIdLabel = function( id ) { 			var arr, label; 			for ( arr of Config.MODULE_TABLES.q_ids ) 				if ( arr ) label = label || arr[ id ]; 			return label || id; 		};  		// format Wikidata quantity 		var wb_getAmount = function( amount ) { 			var a = '' + amount; 			if ( Config.STRINGS.decimalPoint !== '.' ) 				a = a.replace( /\./g, Config.STRINGS.decimalPoint ); 			return a.replace( /^\+/g, '' ); 		};  		var wb_getUnit = function( unit ) { 			var u = ( '' + unit ).replace( /https?:\/\/www.wikidata.org\/entity\//ig, '' ); 			return u === '1' ? '' : u; 		};  		var getQuantity = function( value ) { 			var val = wb_getAmount( value.amount ); 			if ( val === '0' ) return '0'; 			var unit = wb_getUnit( value.unit ); 			if ( unit === '' ) return val;  			var item = Config.MODULE_TABLES.currencies[ unit ]; 			if ( item ) 				unit = item.f || ( '%s ' + item.iso ); 			else 				unit = '%s ' + wb_getIdLabel( unit ); 			return unit.replace( /%s/g, val ).replace( /&nbsp;/g, ' ' ); 		};  		var getHours = function( statement ) { 			function getItems( parts, prop1, prop2 ) { 				var arr = [], end, i, start; 				var count = Math.max( parts[ prop1 ].length, parts[ prop2 ].length ); 				for ( i = 0; i < count; i++ ) { 					start = parts[ prop1 ][ i ]; 					end = parts[ prop2 ][ i ]; 					if ( start && end ) 						arr.push( start + '–' + end ); 					else 						arr.push( start || end ); 				} 				return arr.join( ',' ); 			}  			var i, item, parts = {}, property; 			var result = wb_getIdLabel( statement.value.id );  			var dayOpen = Config.PROPERTIES.dayOpen; 			var dayClosed = Config.PROPERTIES.dayClosed; 			var hourOpen = Config.PROPERTIES.hourOpen; 			var hourClosed = Config.PROPERTIES.hourClosed;  			if ( statement.qualifiers ) { 				for ( property of Config.COMMENTS.hours ) { 					parts[ property ] = []; 					if ( statement.qualifiers[ property ] ) 						for ( item of statement.qualifiers[ property ] ) 							if ( item.snaktype === 'value' && item.datavalue.type === 'wikibase-entityid' ) 								parts[ property ].push( wb_getIdLabel( item.datavalue.value.id ) ); 				} 				item = getItems( parts, hourOpen, hourClosed ); 				if ( item !== '' ) result += ' ' + item; 				item = getItems( parts, dayOpen, dayClosed ); 				if ( item !== '' ) result += ' (' + item + ')'; 			} 			return result; 		};  		var getComments = function( qualifiers, properties ) { 			if ( typeof( qualifiers ) == 'undefined' ) return ''; 			var comments = [], item, minAge, maxAge, property, value; 			var minimumAge = Config.PROPERTIES.minimumAge; 			var maximumAge = Config.PROPERTIES.maximumAge; 			for ( property of properties ) { 				if ( typeof( qualifiers[ property ] ) == 'undefined' ) continue;  				if ( property === minimumAge ) 					minAge = getQuantity( qualifiers[ property ][ 0 ].datavalue.value ); 				else if ( property === maximumAge ) 					maxAge = getQuantity( qualifiers[ property ][ 0 ].datavalue.value ); 				else 					for ( item of qualifiers[ property ] ) 						if ( item.snaktype === 'value' ) { 							value = item.datavalue.value; 							switch( item.datavalue.type ) { 								case 'monolingual': 									value = value.text; 									break; 								case 'wikibase-entityid': 									value = wb_getIdLabel( value.id ); 									break; 							} 							if ( typeof( value ) === 'string' && value !== '' ) 								comments.push( value ); 							 						} 			}  			if ( minAge && maxAge ) 				comments.push( Config.STRINGS.fromTo 					.replace( '%s', parseInt( minAge ) ).replace( '%s', maxAge ) ); 			else if ( minAge ) 				comments.push( Config.STRINGS.from.replace( '%s', minAge ) ); 			else if ( maxAge ) 				comments.push( Config.STRINGS.to.replace( '%s', maxAge ) );  			return ( comments.length === 0 ) ? '' : ' (' + comments.join( ', ' ) + ')'; 		};  		// parse the wikidata "claim" object from the wikidata response 		var wb_getStatements = function( id, jsonObj, claim ) { 			if ( claim.type === 'label' ) { 				var labels = wb_getLabels( id, jsonObj ); 				if ( labels ) { 					if ( claim.which === 'wiki' && labels.wiki && labels.wiki !== '' ) 						return labels.wiki; 					if ( claim.which === 'local' && labels.local && labels.local !== '' ) 						return labels.local; 				} 				return null; 			}  			var entity = wb_checkEntity( id, jsonObj ); 			if ( !entity || !entity.claims ) 				return null;  			var count, lang, pos, property, properties, val, values, results = [], 				statement, statements;  			properties = typeof claim.p == 'string' ? [ claim.p ] : claim.p; 			for ( property of properties ) { 				statements = wb_getBestStatements( entity.claims, property ); 				if ( statements.length === 0 ) 					continue; 				claim.max = claim.max || 1; 				if ( claim.max < statements.length ) 					statements.splice( claim.max, statements.length );  				switch( claim.type ) { 					case 'monolingual': 						values = {}; 						for ( statement of statements ) { 							lang = statement.value.language; 							pos = lang.indexOf( '-' ); 							if ( pos >= 0 ) 								lang = lang.substr( 0, pos ); 							values[ lang ] = statement.value.text; 						} 						if ( claim.which == 'wiki' ) 							for ( lang of Config.SYSTEM.searchLang ) { 								val = values[ lang ]; 								if ( val ) { 									results.push( val ); 									break; 								} 							} 						else { 							val = values[ Config.SYSTEM.localLang ]; 							if ( val ) 								results.push( val ); 						} 						break; 					case 'au': // fees 						for ( statement of statements ) 							results.push( getQuantity( statement.value ) 								+ getComments( statement.qualifiers, Config.COMMENTS.fee ) ); 						break; 					case 'subtype': 					case 'id': 						for ( statement of statements ) { 							if ( typeof claim.table == 'object' ) 								if ( claim.table[ statement.value.id ] ) { 									// subtype 									count = 1; 									var quantity = Config.PROPERTIES.quantity; 									if ( statement.qualifiers && statement.qualifiers[ quantity ] ) { 										count = parseInt( getQuantity( statement.qualifiers[ quantity ][ 0 ].datavalue.value ) ); 										if ( typeof( count ) != 'number' || count < 2 ) 											count = 1; 									} 									val = claim.table[ statement.value.id ]; 									if ( count > 1 ) val += ':' + count; 									results.push( val ); 								} else 									results.push( wb_getIdLabel( statement.value.id ) ); 							else 								results.push( wb_getIdLabel( statement.value.id ) ); 						} 						break; 					case 'hours': 						for ( statement of statements ) { 							val = getHours( statement ); 							if ( val !== '' ) results.push( val ); 						} 						break; 					default: 						for ( statement of statements ) { 							switch( claim.type ) { 								case 'coordinate': 									if ( claim.which == 'latitude' ) 										val = Math.round( statement.value.latitude * 1E6 ) / 1E6; 									else 										val = Math.round( statement.value.longitude * 1E6 ) / 1E6; 									break; 								case 'email': 								case 'contact': 									val = statement.value.replace( 'mailto:', '' ) 										+ getComments( statement.qualifiers, Config.COMMENTS.contact ); 									break; 								default: 									val = statement.value; 							} 							results.push( val ); 						} 				} // switch type 			} // for property  			if ( results.length === 0 ) 				return null; 			else { 				if ( claim.result && claim.result == 'table' ) 					return results; 				else			 					return results.join( ', ' ); 			} 		};  		// expose public members 		return { 			API_WIKIDATA: API_WIKIDATA, 			API_COMMONS: API_COMMONS, 			initializeSisterSiteAutocomplete: initializeSisterSiteAutocomplete, 			ajaxSisterSiteSearch: ajaxSisterSiteSearch, 			wb_getEntity: wb_getEntity, 			wb_getLabels: wb_getLabels, 			wb_getStatements: wb_getStatements 		}; 	}();  // ----------------------------------- Core -----------------------------------  	/** 		Core contains code that should be shared across different 		Wikivoyage languages.  This code uses the custom configurations in the 		Config and Callback modules to initialize 		the listing editor and process add and update requests for listings. 	*/  	var Core = function() { 		var api = new mw.Api(); 		var MODE_ADD = 'add'; 		var MODE_EDIT = 'edit'; 		var ELEMENTS = {};  		// selector that identifies the edit link as created by the 		// addEditButtons() function 		var EDIT_LINK_SELECTOR = '.listing-edit-button a'; 		var SAVE_FORM_SELECTOR = '#progress-dialog'; 		var LOADING_FORM_SELECTOR = '#loading-dialog'; 		var CAPTCHA_FORM_SELECTOR = '#captcha-dialog';  		var addButtonSelector = 'listing-add-button'; 		var contentSelector = ''; 		var displayBlock = false; 		var firstType = ''; 		var inlineListing, inlineDetected, 			replacements = {}, selectComments = {}, sectionText;  		/** 			Form additions before populating the form inputs 		*/  		var additionsToForm = function(mode, clicked, form) { 			var c, data, dataFor, obj, t;  			var listing = clicked.closest( '.vcard' ), body = $( 'body' ); 			var getAttr = function( attr ) { 				var d = mode === MODE_EDIT ? listing.attr( attr ) : null; 				return d || body.attr( attr ) || ''; 			};  			var addAlias = function( tab, aliasObj, indx ) { 				if ( !tab || !aliasObj || !aliasObj.alias ) return;  				var t = aliasObj[ indx ].replace( /[_\s]+/g, '_' ); 				if ( typeof( aliasObj.alias ) === 'string' ) 					tab[ aliasObj.alias ] = t; 				else 					for ( var alias of aliasObj.alias ) 						tab[ alias ] = t; 			};  			var addOption = function( selector, value, label ) { 				selector.append( $( '<option></option>' ).attr( 'value', value ).text( label ) ); 			};  			var addChar = function( char, dataFor, title, dataType ) { 				return mw.format( ' <span class="editor-charinsert" data-for="$1" data-type="$2"><a href="javascript:" title="$3">$4</a></span>', 					dataFor, dataType || '', title, char );	 			};  			Callbacks.setDefaultPlaceholders( form );  			// setting search languages 			var localLang = getAttr( 'data-lang' ); 			Config.SYSTEM.localLang = ''; 			if ( Config.SYSTEM.wikiLang != localLang ) 				Config.SYSTEM.localLang = localLang; 			Config.SYSTEM.searchLang = [ Config.SYSTEM.wikiLang ]; 			for ( c of Config.SYSTEM.addSearchLang ) 				if ( c != Config.SYSTEM.wikiLang && c != localLang ) 					Config.SYSTEM.searchLang.push( c );  			// adding language to local names 			$( '.editor-foreign', form ).attr( 'dir', getAttr( 'data-dir' ) ).attr( 'lang', localLang ); 			$( '.addLocalLang', form ).each( function() { 				$( this ).parent().append( $( '<div class="input-other editor-local-lang"></div>' ) ); 			}); 			data = getAttr( 'data-lang-name' ); 			if ( data !== '' && localLang != Config.SYSTEM.wikiLang ) 				$( '.editor-local-lang', form ).text( data );  			// adding national and international currency symbols 			$( '.addCurrencies', form ).each( function() { 				$( this ).parent().append( $( '<div class="input-other currency-chars"></div>' ) ); 			});  			var html = ''; 			data = getAttr( 'data-currency' ); 			if ( data !== '' ) { 				var natlCurrencies = data.split( ',' ).map( function( item ) { 					return addChar( item.trim(), 'input-price', Config.STRINGS.natlCurrencyTitle, 'currency-char' ); 				}); 				if ( natlCurrencies.length ) 					html += natlCurrencies.join( '' ) + ' |'; 			} 			for ( c of Config.CHARS.intlCurrencies ) 				html += addChar( c, 'input-price', Config.STRINGS.intlCurrencyTitle, 'currency-char' ); 			$( '.currency-chars', form ).append( html );  			// adding country and local calling code 			$( '.addCC', form ).each( function() { 				var _this = $( this ); 				_this.parent().append( 					$( mw.format( '<div class="input-other input-cc$1" data-for="$2"></div>', 						_this.hasClass( 'addLocalCC' ) ? ' input-cc-local' : '', _this.attr( 'id' ) ) ) ); 			});  			var ccLocal = []; 			data = getAttr( 'data-local-calling-code' ); 			if ( data !== '' ) { 				var trunkPrefix = getAttr( 'data-trunk-prefix' ); 				ccLocal = data.split( ',' ).map( function( item ) { 					item = item.trim(); 					// adding trunk prefix if missing 					if ( trunkPrefix !== '' && item.substr( 0, trunkPrefix.length ) !== trunkPrefix ) 						item = trunkPrefix + item; 					return item; 				}); 			}  			data = getAttr( 'data-country-calling-code' ); 			if ( data !== ''  || ccLocal.length > 0 ) { 				$( '.input-cc', form ).each( function() { 					html = ''; 					dataFor = $( this ).attr( 'data-for' ); 					if ( data !== '' ) 						html += addChar( data, dataFor, Config.STRINGS.callingCodeTitle, 'phone-char' ); 					$( this ).append( html ); 				}); 				$( '.input-cc-local', form ).each( function() { 					html = ''; 					dataFor = $( this ).attr( 'data-for' ); 					for ( c of ccLocal ) { 						// exception for Italy and San Marino 						if ( data !== '+39' && data !== '+378' ) 							c = c.replace(/^0/ig, '(0)'); 						html += addChar( c, dataFor, Config.STRINGS.callingCodeTitle, 'phone-char' ); 					} 					$( this ).append( html ); 				}); 			}  			// adding counter and special chars to description label 			html = '<br /><br />'; 			for ( c of Config.CHARS.specialChars ) 				html += addChar( c, 'input-description', Config.STRINGS.specialCharsTitle ); 			$( '#div_description label', form ).parent() 				.append( $( '<br /><span id="counter-description"></span>' ) ) 				.append( html );  			// populating select fields 			for ( obj of Config.MODULE_TABLES.types ) { 				addOption( Core.ELEMENTS.type, obj.type, obj.label ); 				Config.MODULE_TABLES.typeList[ obj.type ] = obj.label; // label is dummy value 				addAlias( Config.MODULE_TABLES.typeAliases, obj, 'type' ); 			}  			for ( obj of Config.MODULE_TABLES.groups ) { 				if ( !obj.is ) 					addOption( Core.ELEMENTS.group, obj.group, obj.label ); 				Config.MODULE_TABLES.groupList[ obj.group ] = obj.label; 				addAlias( Config.MODULE_TABLES.groupAliases, obj, 'group' ); 			} 			for ( obj of Config.MODULE_TABLES.groups ) 				if ( obj.is && obj.is === 'color' ) 					addOption( Core.ELEMENTS.group, obj.group, obj.label );  			var subtypeQualifiers = {}; 			if ( Config.MODULE_TABLES.subtypes ) 				for ( obj of Config.MODULE_TABLES.subtypes ) { 					addOption( Core.ELEMENTS.subtype, obj.type, obj.n 						.replace( /\[([^\[\]]*)\|([^\[\]]*)\]/ig, '$2' ) 						.replace( /\[([^\[\]]*)\]/ig, '' ) ); 					Config.MODULE_TABLES.subtypeList[ obj.type ] = { g: obj.g, wd: obj.wd, n: obj.n, f: obj.f }; 					addAlias( Config.MODULE_TABLES.subtypeAliases, obj, 'type' ); 					if ( typeof obj.wd === 'string' ) 						subtypeQualifiers[ obj.wd ] = obj.type; 					else 						for ( t of obj.wd ) subtypeQualifiers[ t ] = obj.type; 				} 			Config.WIKIDATA_CLAIMS.subtype.table = subtypeQualifiers; 		};  		var splitParameters = function( parameter, table, aliases, aliases2, form, selector ) { 			parameter = parameter.toLowerCase() 				.split( ',' ).map( function( item ) { 					return item.trim(); 				}); 			// translate aliases to types 			for ( var i in parameter ) { 				parameter[ i ] = parameter[ i ].replace(/[_\s]+/g, '_'); 				if ( aliases2 && aliases2[ parameter[ i ] ] ) 					parameter[ i ] = aliases2[ parameter[ i ] ]; 				if ( aliases && aliases[ parameter[ i ] ] ) 					parameter[ i ] = aliases[ parameter[ i ] ]; 			} 			// remove duplicates 			parameter = parameter.filter( function( value, index, self ) { 				return self.indexOf( value ) === index; 			}); 			for ( i = parameter.length - 1; i >= 0; i-- ) { 				// remove empty items 				if ( !parameter[ i ] || parameter[ i ] === '' ) { 					parameter.splice( i, 1 ); 					continue; 				} 				// handle unknown items (custom types) 				if ( !table[ parameter[ i ] ] ) { 					if ( !selector || selector === '' ) 						parameter.splice( i, 1 ); 					else 						$( selector, form ) 							.append('<option value="' + parameter[ i ] + '">' + parameter[ i ] + '</option>'); 				} 			} 			return parameter; 		};  		var checkShowOptions = function( parameter ) { 			var options = {}, i, par; 			for ( par of parameter ) 				options[ par ] = 'o'; 			if ( options.poi && options.coord && !options.all ) { 				options.all = 'o'; 				parameter.push( 'all' ); 			} 			for ( i = parameter.length - 1; i >= 0; i-- ) { 				if ( ( options.none || options.all ) && 					( parameter[ i ] === 'poi' || parameter[ i ] === 'coord' ) ) 					parameter.splice( i, 1 ); 				if ( options.none && parameter[ i ] === 'all' ) 					parameter.splice( i, 1 ); 				if ( options.inline && parameter[ i ] === 'outdent' ) 					parameter.splice( i, 1 ); 			} 			return parameter; 		};  		/** 			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, listingAsMap, clicked ) { 			var form = $( Config.EDITOR_FORM_HTML );  			for ( var parameter in Config.PARAMETERS ) 				ELEMENTS[ parameter ] = $( '#' + Config.getInputId( parameter ), form );  			additionsToForm( mode, clicked, form );  			// multiple select lists 			var l = listingAsMap.type || ''; 			l = splitParameters( l, Config.MODULE_TABLES.typeList, 				Config.MODULE_TABLES.typeAliases, Config.MODULE_TABLES.groupAliases, form, '#input-type' ); 			if ( l.length ) firstType = l[ 0 ]; 			listingAsMap.type = l;  			l = listingAsMap.group; 			if ( l && Config.MODULE_TABLES.groupAliases[ l ] ) 				listingAsMap.group = Config.MODULE_TABLES.groupAliases[ l ]; 			if ( l && l !== '' && !Config.MODULE_TABLES.groupList[ l ] ) 				Core.ELEMENTS.group.append( '<option value="' + l + '">' + l + '</option>' );  			l = listingAsMap.subtype || ''; 			l = splitParameters( l, Config.MODULE_TABLES.subtypeList, 				Config.MODULE_TABLES.subtypeAliases, null, form, '#input-subtype' ); 			listingAsMap.subtype = l;  			l = listingAsMap.show || ''; 			l = splitParameters( l, Config.SHOW_OPTIONS, null, null, form, null ); 			l = checkShowOptions( l ); 			listingAsMap.show = l;  			l = listingAsMap.name || ''; 			if ( l === '' && mode === MODE_EDIT ) 				listingAsMap.name = clicked.closest( '.vcard' ).attr( 'data-name' ) || ''; 			if ( listingAsMap.wikidata && !listingAsMap.auto ) 				listingAsMap.auto = 'y';  			// populate the empty form with existing values 			for ( parameter in Config.PARAMETERS ) { 				var parameterInfo = Config.PARAMETERS[ parameter ]; 				if ( listingAsMap[ parameter] ) 					ELEMENTS[ parameter ].val( listingAsMap[ parameter ] ); 				else if ( parameterInfo.hideDivIfEmpty ) 					$( '#' + parameterInfo.hideDivIfEmpty, form ).hide(); 			} 			for ( var f of Callbacks.CREATE_FORM_CALLBACKS ) 				f( 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 getContentSelector = function() { 			var containers = [ '#bodyContent', '#mw_contentwrapper' ]; // Vector, Modern 			for ( var c of containers ) 				if ( $( c ).length ) { 					contentSelector = c; 					break; 				} 			return contentSelector; 		};  		var wrapContent = function() { 			unwrapContent(); 			$( contentSelector + ' h2' ).each( function() { 				if ( $( this ).closest( '.toctitle' ).length === 0 ) // not for toc 					$( this ).nextUntil( 'h1, h2') 						.addBack() 						.wrapAll( '<div class="mw-h2section" />' ); 			}); 			$( contentSelector + ' h3' ).each( function() { 				$( this ).nextUntil( 'h1, h2, h3' ) 					.addBack() 					.wrapAll( '<div class="mw-h3section" />' ); 			}); 		};  		var unwrapContent = function() { 			$( '.mw-h3section' ).replaceWith( function() { 				return $( this ).contents(); 			}); 			$( '.mw-h2section' ).replaceWith( function() { 				return $( this ).contents(); 			}); 		};  		/** 			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 ( $( Config.DISALLOW_ADD_LISTING_IF_PRESENT.join( ',' ) ).length ) 				return false; 			wrapContent(); 			for ( var sectionId in Config.SECTION_TO_DEFAULT_TYPE ) { 				// do not search using "#id" for two reasons.  one, the article might 				// re-use the same heading elsewhere and thus have two of the same ID. 				// two, unicode headings are escaped ("è" becomes ".C3.A8") and the dot 				// is interpreted by JQuery to indicate a child pattern unless it is 				// escaped 				var topHeading = $( 'h2 [id="' + sectionId + '"]' ); 				if ( topHeading.length ) { 					insertAddListingPlaceholder( topHeading ); 					var parentHeading = topHeading.closest( 'div.mw-h2section' ); 					insertAddListingPlaceholder( $( 'h3 .mw-headline', parentHeading ) ); 				} 			} 			unwrapContent(); 		};  		/** 			Append the "add listing" link text to a heading. 		*/ 		var insertAddListingPlaceholder = function( parentHeading ) { 			var editSections = $( parentHeading ).next( '.mw-editsection' ); 			if ( !editSections.length ) return;  			var isMinerva = $( 'body.skin-minerva' ).length, 				bClass = addButtonSelector; 			if ( isMinerva ) 				bClass = 'mw-ui-icon mw-ui-icon-element ' + bClass; 			var addButton = buttonLink( Config.STRINGS.add, 				Config.STRINGS.addTitle, bClass, MODE_ADD ); 			if ( isMinerva ) { 				// Mobile view: Minerva support 				addButton = $( '<span />' ).append( addButton ); 				editSections = $( parentHeading ).next( 'span' ); 				editSections.after( addButton ); 			} else { 				editSections.append( '<span class="mw-editsection-bracket">[ </span>' ); 				editSections.append( addButton ); 				editSections.append( '<span class="mw-editsection-bracket">]</span>' ); 			} 		};  		var buttonLink = function( text, title, bClass, mode ) { 			return $( '<a href="javascript:" />' ) 				.addClass( bClass || '' ) 				.attr( 'title', title ) 				.text( text ) 				.click( function() { 					initListingEditorDialog( mode, $( this ) ); 				}); 		};  		/** 			Place an "edit" link next to all existing listing tags. 		*/ 		var addEditButtons = function() { 			var editButton = buttonLink( Config.STRINGS.edit, 				Config.STRINGS.editTitle, '', MODE_EDIT ); 			editButton = $( '<span />' ).append( editButton ); 			editButton = $( '<span class="listing-metadata-item listing-edit-button noprint"></span>' ) 				.append( editButton ); 			$( Config.SELECTORS.listingEditLink ).append( editButton ); 		};  		/** 			Determine whether a listing entry is within a paragraph rather than 			an entry in a list 		*/ 		var isInline = function( entry ) { 			return entry.closest( 'p' ).length && entry.closest( 'span.vcard' ).length; 		};  		/** 			Given a DOM element, find the nearest editable section (h2 or h3) that 			it is contained within. 		*/ 		var findSectionHeading = function(element) { 			return element.closest( 'div.mw-h3section, div.mw-h2section' ); 		};  		/** 			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;  			// Vector etc. skins 			var link = heading.find('.mw-editsection a').attr('href'); 			var section = (link !== undefined) ? link.split('=').pop() : 0; 			if (section > 0) return section;  			// MinervaNeue 			link = heading.find('a[data-section]'); 			section = link.attr('data-section'); 			if (section !== undefined) return section;  			// Mobile view: Minerva support 			link = heading.find('.section-heading 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 Config.SECTION_TO_DEFAULT_TYPE) 				if (sectionType == sectionId) 					return Config.SECTION_TO_DEFAULT_TYPE[sectionId]; 			return Config.TEMPLATES[ 0 ]; 		};  		var replaceSpecial = function(str) { 			return str.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); 		};  		/** 			Return a regular expression that can be used to find all listing 			template invocations (as configured via the 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() { 			return new RegExp('({{\\s*(' + Config.TEMPLATES.join('|') + ')\\b)(\\s*[\\|}])','ig'); 		};  		/** 			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 listingSyntax, regexResult, listingMatchIndex; 			for (var i = 0; i <= listingIndex; i++) { 				regexResult = listingRegex.exec(sectionText); 				listingMatchIndex = regexResult.index; 				listingSyntax = regexResult[1]; 			} 			// listings may contain nested templates, so step through all section 			// text after the matched text to find MATCHING closing braces 			// the first two braces are matched by the listing regex and already 			// captured in the listingSyntax variable 			var curlyBraceCount = 2; 			var endPos = sectionText.length; 			var startPos = listingMatchIndex + listingSyntax.length; 			var matchFound = false; 			for (var j = startPos; j < endPos; j++) { 				if (sectionText[j] === '{') 					++curlyBraceCount; 				else if (sectionText[j] === '}') 					--curlyBraceCount; 				if (curlyBraceCount === 0 && (j + 1) < endPos) { 					listingSyntax = sectionText.substring(listingMatchIndex, j + 1); 					matchFound = true; 					break; 				} 			} 			if (!matchFound) 				listingSyntax = sectionText.substring(listingMatchIndex); 			return (listingSyntax || '').trim(); 		};  		/** 			Check if only yes or no is entered. 		*/ 		var checkYesNo = function( value ) { 			var v = value.toLowerCase(); 			return Config.STRINGS.yes.includes( v ) ? 'y' : 				( Config.STRINGS.no.includes( v ) ? 'n' : '' ); 		};  		/** 			Convert raw wiki listing syntax into a mapping of key-value pairs 			corresponding to the listing template parameters. 		*/ 		var wikiTextToListing = function( listingWikiSyntax ) { 			var typeRegex = getListingTypesRegex(), comments, key; 			// convert "{{see|" to {{listing|" 			listingWikiSyntax = listingWikiSyntax 				.replace( typeRegex, '{{' + Config.TEMPLATES[ 0 ] + '$3' ) 				.slice(0,-2); // remove the trailing braces 			var listingAsMap = parseListing( listingWikiSyntax ); 			// replace comment placeholders by its original values 			for ( key in listingAsMap ) 				listingAsMap[ key ] = restoreComments(listingAsMap[ key ], false);  			// remove comments from select list and store it 			for ( key in Config.PARAMETERS ) 				if ( listingAsMap[ key ] && listingAsMap[ key ] !== '' 					&& Config.PARAMETERS[ key ].tp && Config.PARAMETERS[ key ].tp === 'select' ) { 					comments = listingAsMap[ key ].match( /<!--.*?-->/g ); 					if ( comments ) { 						selectComments[ key ] = comments; 						listingAsMap[ key ] = Callbacks.removeComments( listingAsMap[ key ] ); 					} 				} 			// convert paragraph tags to newlines 			if ( listingAsMap.description && displayBlock ) 				listingAsMap.description = listingAsMap.description.replace(/\s*<p>\s*/g, '\n\n'); 			// remove control characters 			for ( key in listingAsMap ) 				listingAsMap[ key ] = removeCtrls( listingAsMap[ key ], key == 'description' );  			// sanitize the listing type param to match the configured values, so 			// if the listing contained "Do" it will still match the configured "do" 			if ( !listingAsMap.type ) 				listingAsMap.type = ''; 			for ( key of Config.TEMPLATES ) 				if ( listingAsMap.type.toLowerCase() === key.toLowerCase() ) { 					listingAsMap.type = key; 					break; 				} 			for ( key in listingAsMap ) { 				var c = checkYesNo( listingAsMap[ key ] ); 				if ( c !== '' ) listingAsMap[ key ] = c; 			}  			// copying parameter aliases if possible 			var arr, j, key2; 			for ( key in Config.PARAMETERS ) { 				arr = Config.PARAMETERS[ key ].aliases || []; 				for ( key2 of arr ) { 					if ( ( !listingAsMap[ key ] || listingAsMap[ key ] === '' ) 						&& listingAsMap[ key2 ] ) { 						listingAsMap[ key ] = listingAsMap[ key2 ]; 						delete( listingAsMap[ key2 ] ); 					} 				} 			}  			return listingAsMap; 		};  		/** 			Split the raw template wikitext into an array of params. The pipe 			symbol delimits template params, but this method will also inspect the 			content to deal with nested templates or wikilinks that might contain 			pipe characters that should not be used as delimiters. 		*/  		// masking pipes in templates and wiki links by \x00 		var maskPipes = function( s ) { 			var regex1, regex2, regex3, regex4, t; 			function replacePipes( name, offset, str ) { 				return name.replace( /\|/g, '\x00' ).replace( regex2, '\x01' ).replace( regex3, '\x02' ); 			} 			function masking( str, start, end ) { 				regex1 = new RegExp( '\\' + start + '{2}[^\\' + start + '\\' + end + ']*\\' + end + '{2}', 'g' ); 				regex2 = new RegExp( '\\' + start, 'g' ); 				regex3 = new RegExp( '\\' + end, 'g' ); 				regex4 = new RegExp( '\\' + end + '{2}$' );  				str += end + end; 				do { 					t = str; 					str = str.replace( regex1, replacePipes ); 				} while ( t !== str ); 				return str.replace( regex4, '' ).replace( /\x01/g, start ).replace( /\x02/g, end ); 			} 			s = masking( s, '{', '}' ); 			return masking( s, '[', ']' ); 		};  		var parseListing = function( listingWikiSyntax ) { 			var listingAsMap = {}; 			var str = listingWikiSyntax.replace( /[\x00-\x02]/g, '' ).slice( 2 ); // remove {{ 			str = maskPipes( str );  			// splitting each parameter 			var results = str.split( '|' ); 			results.shift(); 			var at, index = 1, match, name, result; 			for ( result of results ) { 				result = result.trim().replace( /\x00/g, '|' ); 				match = result.match( /[^<=\{\[]*\s*=/ ); 				if ( match && match[ 0 ] !== '=' ) { 					at = match[ 0 ].length; 					name = match[ 0 ].substr( 0, at - 1 ) 						.replace( /[\x00-\x0F\x7F]+/g, '') 						.replace( / +/g, ' ').trim(); 					listingAsMap[ name ] = result.substr( at ).trim(); 				} else { 					listingAsMap[ '' + index ] = result.replace( /^=/, '' ).trim(); 					index++; 				} 			}  			return listingAsMap; 		};  		/** 			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 ) { 			wrapContent(); 			progressForm(LOADING_FORM_SELECTOR,Config.STRINGS.loading); 			var listingType, sel; 			if ( mode === MODE_ADD ) 				listingType = findListingTypeForSection( clicked ); 			else { 				sel = clicked.closest( '.vcard' ); 				listingType = sel.attr( 'data-type' ); 				displayBlock = sel.prop( 'tagName' ) === 'DIV'; 			} 			var sectionHeading = findSectionHeading(clicked); 			var sectionIndex = findSectionIndex(sectionHeading); 			var listingIndex = (mode === MODE_ADD) ? -1 : findListingIndex(sectionHeading, clicked); 			inlineDetected = mode === MODE_EDIT && isInline( clicked ); 			inlineListing = Config.OPTIONS.inlineFormat || inlineDetected;  			// Following code is jQuery 3.x compatible 			$.ajax({ 				url: mw.util.wikiScript(''), 				data: { title: mw.config.get('wgPageName'), action: 'raw', section: sectionIndex }, 				cache: false, // required 				timeout: 3000 			}).done(function(data, textStatus, jqXHR) { 				sectionText = data; 				openListingEditorDialog(mode, sectionIndex, listingIndex, listingType, clicked); 			}).fail(function(jqXHR, textStatus, errorThrown) { 				closeForm(LOADING_FORM_SELECTOR); 				alert(Config.STRINGS.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, clicked) { 			sectionText = stripComments(sectionText); 			// Not working in Minerva skin because of missing modules  			var listingAsMap = {}, listingWikiSyntax, t; 			if ( mode == MODE_ADD ) 				listingAsMap.type = listingType; 			else { 				listingWikiSyntax = getListingWikitextBraces(listingIndex); 				listingAsMap = wikiTextToListing( listingWikiSyntax ); 				t = listingAsMap.type; 				if ( listingType && ( !t || t === "" ) ) 					listingAsMap.type = listingType; 				listingType = listingAsMap.type; 			} 			// if a listing editor dialog is already open, get rid of it 			closeForm(Config.SELECTORS.editorForm); 			closeForm(LOADING_FORM_SELECTOR); 			// wide dialogs on huge screens look terrible 			var windw = $( window ); 			var dialogWidth = (windw.width() > Config.OPTIONS.MaxDialogWidth) ? Config.OPTIONS.MaxDialogWidth : 'auto'; 			var form = $( createForm( mode, listingAsMap, clicked ) ); 			form.dialog({ 				// modal form - must submit or cancel 				modal: true, 				height: 'auto', 				width: dialogWidth, 				title: (mode == MODE_ADD) ? Config.STRINGS.addTitle : Config.STRINGS.editTitle, 				dialogClass: 'listingeditor-dialog', 				close: function() { 					unwrapContent(); 				}, 				buttons: [ 				{ 					text: Config.STRINGS.help, 					title: Config.STRINGS.helpTitle, 					id: 'listingeditor-help', 					click: function() { window.open(Config.STRINGS.helpPage);} 				}, 				{ 					text: Config.STRINGS.submit, 					title: Config.STRINGS.submitTitle, 					click: function() { 						if ($(Config.SELECTORS.editorDelete).is(':checked')) { 							// no validation 							formToText(mode, listingWikiSyntax, listingAsMap, sectionNumber, false); 							$(this).dialog('close'); 						} 						else if (validateForm()) { 							formToText(mode, listingWikiSyntax, listingAsMap, sectionNumber, true); 							$(this).dialog('close'); 						} 					} 				}, 				{ 					text: Config.STRINGS.preview, 					title: Config.STRINGS.previewTitle, 					id: 'listingeditor-preview-button', 					click: function() { 						startPreview( listingAsMap ); 					} 				}, 				{ 					text: Config.STRINGS.previewOff, 					title: Config.STRINGS.previewOffTitle, 					id: 'listingeditor-preview-off', 					style: 'display: none', 					click: function() { 						togglePreview( true ); 					} 				}, 				{ 					text: Config.STRINGS.refresh, 					title: Config.STRINGS.refreshTitle, 					icon: 'ui-icon-refresh', 					id: 'listingeditor-refresh', 					style: 'display: none', 					click: function() { 						refreshPreview( listingAsMap ); 					} 				}, 				{ 					text: Config.STRINGS.cancel, 					title: Config.STRINGS.cancelTitle, 					click: function() { 						if ( checkForChanges( listingAsMap ) || confirm( Config.STRINGS.cancelMessage ) ) { 							$(this).dialog('destroy').remove(); 							unwrapContent(); 						} 					} 				} 				], 				create: function() { 					$('.ui-dialog-buttonpane').append('<div class="listingeditor-license">' + Config.STRINGS.licenseText 						+ '<span class="listingeditor-version"> (' + Config.SYSTEM.version + ')</span>' + '</div>'); 				} 			});  			var windowHeight = windw.height(); 			if ( windowHeight < 720 ) { 				var fontSize = parseFloat( $( '.listingeditor-dialog' ).css( 'font-size' ) ); 				$( '.listingeditor-dialog' ) 					.css( 'font-size', fontSize * windowHeight / 720 ); 				fontSize = parseFloat( $( '.chosen-container' ).css( 'font-size' ) ); 				$( '.chosen-container' ) 					.css( 'font-size', fontSize * windowHeight / 720 ); 			} 		};  		/** 			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 regex = [ /<!--.*?-->/g, /<nowiki>.*?<\/nowiki>/gi, /<pre>.*?<\/pre>/gi ], 				comments, i, j, rep; 			for ( j = 0; j < regex.length; j++ ) { 				comments = text.match( regex[ j ] ); 				if ( comments ) 					for ( i = 0; i < comments.length; i++ ) { 						rep = '<<<COMMENT' + i + ';' + j + '>>>'; 						text = text.replace(comments[ i ], rep); 						replacements[rep] = comments[ i ]; 					} 			} 			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 ) 				text = text.replace(key, replacements[key]); 			if ( resetReplacements ) 				replacements = {}; 			return text; 		};  		/** 			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 f of Callbacks.VALIDATE_FORM_CALLBACKS ) 				f( validationFailureMessages ); 			if ( validationFailureMessages.length ) { 				alert( validationFailureMessages.join( '\n' ) ); 				return false; 			} 			return true; 		};  		/** 			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 getValues = function( listing ) { 			var l = $.extend( true, {}, listing ); 			for ( var parameter in Config.PARAMETERS ) 				l[ parameter ] = ELEMENTS[ parameter ].val(); 			return l; 		};  		var formToText = function( mode, listingWikiSyntax, listingAsMap, sectionNumber, withCallbacks ) { 			var listing = getValues( listingAsMap ); 			if ( withCallbacks ) 				for ( var f of Callbacks.SUBMIT_FORM_CALLBACKS ) 					f( listing, mode ); 			var text = listingToStr( listing ); 			var summary = editSummarySection(); 			var name = listingAsMap.name; 			if ( listing.name.trim() !== '' ) 				name = listing.name.trim(); 			if ( mode == MODE_ADD ) 				summary = updateSectionTextWithAddedListing( summary, text, listing, name ); 			else 				summary = updateSectionTextWithEditedListing( summary, text, listingWikiSyntax, name ); 			if ( $(Config.SELECTORS.editorSummary).val() !== '' ) 				summary += ' – ' + $(Config.SELECTORS.editorSummary).val(); 			var minor = $(Config.SELECTORS.editorMinorEdit).is(':checked') ? true : false; 			saveForm(summary, minor, sectionNumber, '', ''); 		};  		var showPreview = function( listingAsMap ) { 			var text = listingToStr( getValues( listingAsMap ) ); 			$( '#listingeditor-preview-syntax' ).text( text );  			$.ajax ({ 				url: mw.config.get( 'wgScriptPath' ) + '/api.php?' + $.param({ 					action: 'parse', 					prop: 'text', 					contentmodel: 'wikitext', 					format: 'json', 					'text': text, 				}), 				error: function( jqXHR, txt ) { 					$( '#listingeditor-preview' ).hide(); 				}, 				success: function( data ) { 					$( '#listingeditor-preview-text' ).html( data.parse.text[ '*' ] ); 				}, 			}); 		};  		/** 			Preview 		*/ 		var togglePreview = function( visible ) { 			$( '#listingeditor-preview' ).toggle( !visible ); 			$( '#listingeditor-refresh' ).toggle( !visible ); 			$( '#listingeditor-preview-off' ).toggle( !visible ); 			$( '#listingeditor-preview-button' ).toggle( visible ); 		};  		var startPreview = function( listingAsMap ) { 			var visible = $( '#listingeditor-preview' ).is( ':visible' ); 			togglePreview( visible ); 			if ( !visible ) 				showPreview( listingAsMap ); 		}; 		 		var refreshPreview = function( listingAsMap ) { 			if ( $( '#listingeditor-preview' ).is( ':visible' ) ) 				showPreview( listingAsMap ); 		};  		/** 			For cancel button: check if any changes were made for warning msg. 		*/ 		var checkForChanges = function( listingAsMap ) { 			var noChanges = true, i, p, val; 			for ( var parameter in Config.PARAMETERS ) { 				p = listingAsMap[ parameter ]; 				val = ELEMENTS[ parameter ].val(); 				if ( typeof( val ) === 'string' ) { 					if ( val !== ( p || '' ) ) { 						noChanges = false; break; 					} 				} else { // multiple select 					p = p || []; 					if ( val.length !== p.length ) { 						noChanges = false; break; 					} 					for ( i = 0; i < val.length; i++ ) 						if ( !p.includes( val[ i ] ) ) { 							noChanges = false; break; 						} 				} 			} 			return noChanges; 		};  		/** 			Begin building the edit summary by trying to find the section name. 		*/ 		var editSummarySection = function() { 			var sectionName = getSectionName(); 			return ( sectionName.length ) ? '/* ' + sectionName + ' */ ' : ''; 		};  		var getSectionName = function() { 			var HEADING_REGEX = /^=+\s*([^=]+)\s*=+\s*\n/; 			var result = HEADING_REGEX.exec(sectionText); 			return ( result !== null ) ? result[ 1 ].trim() : ''; 		};  		/** 			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, name ) { 			var summary = originalEditSummary + mw.format( Config.STRINGS.added, name ); 			// add the new listing to the end of the section.  if there are 			// sub-sections, add it prior to the start of the sub-sections. 			var index = sectionText.indexOf('==='); 			if (index === 0) 				index = sectionText.indexOf('===='); 			if (index > 0) 				sectionText = sectionText.substr(0, index) + '* ' + listingWikiText 					+ '\n' + sectionText.substr(index); 			else 				sectionText += '\n'+ '* ' + listingWikiText;  			sectionText = restoreComments( sectionText, true ); 			return summary; 		};  		/** 			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, listingWikiSyntax, name ) { 			var summary = originalEditSummary;  			// '$&' like in '$&nbsp;' will be misinterpreted in regex replacements 			listingWikiSyntax = listingWikiSyntax.replace( /\$&/ig, '&#36;&'); 			sectionText = sectionText.replace( /\$&/ig, '&#36;&'); 			listingWikiText = listingWikiText.replace( /\$&/ig, '&#36;&');  			if ( $( Config.SELECTORS.editorDelete ).is( ':checked' ) ) { 				summary += mw.format( Config.STRINGS.removed, name ); 				var listRegex = new RegExp('(\\n+[\\:\\*\\#]*)?\\s*' + replaceSpecial( listingWikiSyntax )); 				sectionText = sectionText.replace( listRegex, '' ); 			} else { 				summary += mw.format( Config.STRINGS.updated, name ); 				sectionText = sectionText.replace( listingWikiSyntax, listingWikiText ); 			} 			sectionText = restoreComments(sectionText, true).replace( /&#36;/ig, '$$' ); // restore $ 			return summary; 		};  		/** 			Render a dialog that notifies the user that the listing editor is 			loaded or changes are being saved. 		*/ 		var closeForm = function(selector) { 			if ( $(selector).length ) 				$(selector).dialog('destroy').remove(); 		};  		var progressForm = function(selector, text) { 			// if a progress dialog is already open, get rid of it 			closeForm(selector); 			var progress = $('<div id="' + selector.replace( '#', '' ) + '">' + text + '</div>'); 			progress.dialog({ 				modal: true, 				height: 110, 				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 ) 				editPayload.minor = 'true'; 			api.postWithToken( 				"csrf", 				editPayload 			).done(function(data, jqXHR) { 				if (data && data.edit && data.edit.result == 'Success') { 					// since the listing editor can be used on diff pages, redirect 					// to the canonical URL if it is different from the current URL 					var canonicalUrl = $("link[rel='canonical']").attr("href"); 					var currentUrlWithoutHash = window.location.href.replace(window.location.hash, ""); 					if (canonicalUrl && currentUrlWithoutHash != canonicalUrl) { 						var sectionName = mw.util.escapeIdForLink(getSectionName()); 						if (sectionName.length) 							canonicalUrl += "#" + sectionName; 						window.location.href = canonicalUrl; 					} else 						window.location.reload(); 				} else if (data && data.error) { 					saveFailed(Config.STRINGS.submitApiError + ' "' + data.error.code + '": ' + data.error.info ); 				} else if (data && data.edit.spamblacklist) { 					saveFailed(Config.STRINGS.submitBlacklistError + ': ' + data.edit.spamblacklist ); 				} else if (data && data.edit.captcha) { 					closeForm(SAVE_FORM_SELECTOR); 					captchaDialog(summary, minor, sectionNumber, data.edit.captcha.url, data.edit.captcha.id); 				} else 					saveFailed(Config.STRINGS.submitUnknownError); 			}).fail(function(code, result) { 				if (code === "http") 					saveFailed(Config.STRINGS.submitHttpError + ': ' + result.textStatus ); 				else if (code === "ok-but-empty") { 					saveFailed(Config.STRINGS.submitEmptyError); 				} else 					saveFailed(Config.STRINGS.submitUnknownError + ': ' + code ); 			}); 			progressForm(SAVE_FORM_SELECTOR,Config.STRINGS.saving); 		};  		/** 			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) { 			closeForm(SAVE_FORM_SELECTOR); 			$(Config.SELECTORS.editorForm).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 			closeForm(CAPTCHA_FORM_SELECTOR); 			var captcha = $('<div id="captcha-dialog">').text(Config.STRINGS.externalLinks); 			var image = $('<img class="fancycaptcha-image">') 				.attr('src', captchaImgSrc) 				.appendTo(captcha); 			var label = $('<label for="input-captcha">').text(Config.STRINGS.enterCaptcha).appendTo(captcha); 			var input = $('<input id="input-captcha" type="text">').appendTo(captcha); 			captcha.dialog({ 				modal: true, 				title: Config.STRINGS.enterCaptcha, 				buttons: [ 					{ 						text: Config.STRINGS.submit, click: function() { 							saveForm(summary, minor, sectionNumber, captchaId, $('#input-captcha').val()); 							$(this).dialog('destroy').remove(); 						} 					}, 					{ 						text: Config.STRINGS.cancel, click: function() { 							$(this).dialog('destroy').remove(); 						} 					} 				] 			}); 		};  		// remove controls and illegeal chars 		var removeCtrls = function( str, isContent ) { 			str = str.trim(); 			if ( str === '' ) return ''; 			if ( displayBlock && isContent ) { 				// remove controls from tags at first 				str = str.replace( /(<[^>]+>)/g, function( name, offset, str ) { 					return name.replace( /[\x00-\x0F\x7F]/g, ' ' ); 				}); 				str = str.replace( /[\x00-\x09\x0B\x0C\x0E\x0F\x7F]/g, ' ' ); 			} else 				str = str.replace( /(<\/?br[^%/>]*\/*>|<\/?p[^%/>]*\/*>)/g, ' ' ) 					.replace( /[\x00-\x0F\x7F]/g, ' ' ); 			return str.trim().replace( / {2,}/g, ' ' ); 		};  		/** 			Convert the listing map back to a wiki text string. 		*/ 		var sortSubtypes = function( s ) { 			return s.sort( function( a, b ) { 				var aa = a.replace( /:.*$/g, '' ); 				var bb = b.replace( /:.*$/g, '' ); 				if ( Config.MODULE_TABLES.subtypeList[ aa ] && Config.MODULE_TABLES.subtypeList[ bb ] ) { 					if ( Config.MODULE_TABLES.subtypeList[ aa ].g < Config.MODULE_TABLES.subtypeList[ bb ].g ) 						return -1; 					if ( Config.MODULE_TABLES.subtypeList[ aa ].g > Config.MODULE_TABLES.subtypeList[ bb ].g ) 						return 1; 				} 				return aa.localeCompare( bb ); 			}); 		};  		var getAlias = function( value, aliases ) { 			for ( var key in aliases ) 				if ( aliases[ key ] === value ) { 					value = key; 					break; 				} 			return value; 		};  		var listingToStr = function( listing ) { 			var arr, i, l, par, keepIt;  			// values cleanup 			for ( var parameter in listing ) { 				l = listing[ parameter ]; 				if ( typeof l == 'object' ) 					for ( i = l.length - 1; i >= 0 ; i-- ) { 						if ( !l[ i ] || l[ i ] === '' ) 							l.splice( i, 1 ); 					} 				else { 					l = removeCtrls( l, parameter == 'description' ) 						.trim() 						.replace( / {2,}/g, ' ' ); 					l = maskPipes( l ).replace( /\|/g, '{{!}}' ).replace( /\x00/g, '|' ); 					// handle punctuation marks 					if ( Config.OPTIONS.withoutPunctuation.includes( parameter ) ) 						l = l.replace( /[.,;!?]+$/, '' ); 					if ( parameter === 'description' && l !== '' && !l.match( /[.!?]$/ ) ) 						l = l + '.'; 				} 				listing[ parameter ] = l; 			}  			var saveStr = '{{' + Config.TEMPLATES[ 0 ] + ' '; 			for ( parameter in Config.PARAMETERS ) { 				// recognized parameters only 				keepIt = Config.PARAMETERS[ parameter ].keepIt 					|| Config.OPTIONS.keepItDefault; 				l = listing[ parameter ];  				switch( parameter ) { 					case 'type': 						if ( firstType !== '' ) 							for ( i = 0; i < l.length; i++ ) 								if ( l[ i ] == firstType ) { 									l.splice( i, 1 ); 									l.unshift( firstType ); 									break; 								} 						if ( Config.OPTIONS.CopyToTypeAliases ) 							for ( i = 0; i < l.length; i++ ) 								l[ i ] = getAlias( l[ i ], Config.MODULE_TABLES.typeAliases ); 						l = l.join( ', ' ).replace( /_/g, ' ' ); 						break; 					case 'group': 						if ( Config.OPTIONS.CopyToTypeAliases ) 							l = getAlias( l, Config.MODULE_TABLES.groupAliases ); 						break; 					case 'subtype': 						// sorting subtypes by groups 						l = sortSubtypes( l ); 						if ( Config.OPTIONS.CopyToTypeAliases ) 							for ( i = 0; i < l.length; i++ ) 								l[ i ] = getAlias( l[ i ], Config.MODULE_TABLES.subtypeAliases ); 						l = l.join( ', ' ).replace( /_/g, ' ' ); 						break; 					case 'show': 						l = checkShowOptions( l ); 						l = l.join( ', ' ).replace( /_/g, ' ' ); 				}  				if ( selectComments[ parameter ] ) 					l = l + selectComments[ parameter ].join( '' ); 				par = parameter; 				arr = Config.PARAMETERS[ par ].aliases || [];  				// renaming parameter 				if (Config.OPTIONS.CopyToAliases && arr[0] && !listing[ arr[0] ]) 					par = arr[0]; 				if ( l !== '' || keepIt ) 					saveStr += '| ' + par + ' = ' + l; 				if ( !saveStr.match( /\n$/ ) ) { 					saveStr = saveStr.replace(/\s+$/, ''); 					saveStr += ( !inlineListing && Config.PARAMETERS[parameter].newline ) 						? '\n' : ' '; 				} 			} 			if ( Config.OPTIONS.AllowUnrecognizedParameters ) 				// append any unexpected values 				for ( parameter in listing ) 					if ( !Config.PARAMETERS[ parameter ] && listing[ parameter ] !== '' ) 						saveStr += '| ' + parameter + ' = ' + listing[ parameter ] 							+ ( inlineListing ) ? ' ' : '\n';  			return inlineDetected ? saveStr.replace( /\s+$/, ' }}' ) : saveStr.replace( /\s+$/, '\n}}' ); 		};  		/** 			Called on DOM ready, this method initializes the listing editor and 			adds the "add/edit listing" links to sections and existing listings. 		*/ 		var init = function() { 			if ( getContentSelector === '' ) return; 			$('head').append('<link rel="stylesheet" type="text/css" href="/w/index.php?title=MediaWiki:ListingEditor.css&action=raw&ctype=text/css">'); 			addEditButtons(); 			addListingButtons(); 		};  		// expose public members 		return { 			ELEMENTS: ELEMENTS, 			MODE_ADD: MODE_ADD, 			MODE_EDIT: MODE_EDIT, 			checkYesNo: checkYesNo, 			firstType: firstType, 			init: init, 			initListingEditorDialog: initListingEditorDialog, 			sortSubtypes: sortSubtypes 		}; 	}();  	$( Core.init );  	return { 		initListingEditorDialog: Core.initListingEditorDialog 	};  } ( mediaWiki, jQuery ) );  //</nowiki>