Benutzer:RolandUnger/ListingEditor.js
Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.
- Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
- Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
- Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
- Opera: Strg+F5
/******************************************************************
Listing Editor v2.3-de
Further development at: Benutzer:RolandUnger/ListingEditor.js
Original author: ausgehe
- torty3
- Wrh2
- RolandUnger
TODO (WV-en):
- Add support for mobile devices.
- wrapContent is breaking the expand/collapse logic on the VFD page.
TODO (WV-de v2.3 and above)
- Substitution of jquery.ui Dialog and jquery.ui Autocomplete modules
- Remove control characters from input
- Check field data entries (URLs, phone numbers, emails, skype, coords,
image, show, subtypes, last edit).
- Check and convert coordinates (hex/dec).
- Show errors with red border.
- Get type from Wikidata (WD).
- Getting country-specific data from article entity (language, currency,
country calling code).
- Show at beginning: Loading...
- Add support for mobile devices.
- Change type list to autocomplete combobox.
- Improved for show and subtype parameters with comboboxes with multiple
selections.
- Picking up gps position from external tools.
v2.3-de Changes:
- ...
v2.2-de Changes:
- Adaption to Wikivoyage/de (translations etc.).
- Getting placeholders from Wikidata.
- Getting Address, Phone, Fax, Email, etc. from Wikidata.
- Populate input-type select list from LISTING_TYPE_OPTIONS.
- Add additional vCard fields.
- Handle templates only in inline mode.
- Handle missing type parameter and vCard template (no type substitution).
- Remove Wikipedia field.
- Preview button for template preview.
- Add mouse cursor back to price after clicking currency sign.
- Extended checks if article can be edited.
- Type dependend show / hide changes.
- Table layout replaced by div layout.
- Gadget: suppress listing editor.
- Label titles added.
- Type and type group with related color sample.
- Accept comma-separated "lat, long" in lat field.
- Do not perform listing validations when deleting a listing.
- Pipe delimiter before edit button will not be printed.
- Adding listing-empty-input, listing-default-placeholder,
listing-wikidata-placeholder classes.
- Several cleanups to the underlying code.
v2.1 Changes:
- Wikidata & Wikipedia fields added.
- The Wikidata, image, and Wikipedia fields will now autocomplete, with
lookups done by searching the relevant site.
- Latitude, longitude, official link, wikipedia link, and image can be
populated with the values stored at Wikidata by clicking on the "Update
shared fields using values from Wikidata" link.
- The "image" field is now shown by default.
- Several cleanups to the underlying code.
v2.0 Changes:
- Update the listing editor dialog UI to provide more space for field
entry, and to responsively collapse from two columns to one on small
screens.
- Use mw.Api().postWithToken instead of $.ajax to hopefully fix session
token expiration issues.
- Add support for editing of multi-paragraph listings.
- Do not delete non-empty unrecognized listing template values (wikipedia,
phoneextra, etc) when editing if the ALLOW_UNRECOGNIZED_PARAMETERS flag
is set to true.
- If listing editor form submit fails, re-display the listing editor form
with the content entered by the user so that work is not lost.
- Replace synchronous $.ajax call with asynchronous.
- Allow edit summaries and marking edits as minor when editing listings.
- Move 'add listing' link within the mw-editsection block.
- Automatically replace newlines in listing content with <p> tags.
- Fix bug where HTML comments inside listing fields prevented editing.
- Provide configuration options so that some fields can be displayed only
if they have non-empty values (examples: "fax" in the default
configuration).
- Add basic email address validation.
- A failed captcha challenge no longer causes further captcha attempts to
fail.
- Significant code reorganization.
********************************************************************/
//<nowiki>
( function ( mw, $ ) {
'use strict';
/* ***********************************************************************
* CUSTOMIZATION INSTRUCTIONS:
*
* Different Wikivoyage language versions have different implementations of
* the listing template, so this module must be customized for each. The
* ListingEditor.Config and ListingEditor.Callbacks modules should be the
* ONLY code that requires customization - ListingEditor.Core should be
* shared across all language versions. If for some reason the Core module
* must be modified, ideally the module should be modified for all language
* versions so that the code can stay in sync.
* ***********************************************************************/
var ListingEditor = {};
// see http://toddmotto.com/mastering-the-module-pattern/ for an overview
// of the module design pattern being used in this gadget
// --------------------------- ListingEditor.Config ---------------------------
/* ***********************************************************************
* ListingEditor.Config contains properties that will likely need to be
* modified for each Wikivoyage language version. Properties in this
* module will be referenced from the other ListingEditor modules.
* ***********************************************************************/
ListingEditor.Config = function() {
// --------------------------------------------------------------------
// TRANSLATE THE FOLLOWING BASED ON THE WIKIVOYAGE LANGUAGE IN USE
// --------------------------------------------------------------------
var LANG = 'de'; // vCard specific
var COMMONS_URL = '//commons.wikimedia.org';
var WIKIDATA_URL = '//www.wikidata.org';
var WIKIPEDIA_URL = '//de.wikipedia.org'; // vCard specific
var WIKIDATA_SITELINK_WIKIPEDIA = 'dewiki'; // vCard specific
var TRANSLATIONS = { // vCard specific
'addTitle' : 'Hinzufügen einer neuen vCard',
'editTitle' : 'Bearbeitung einer vorhanden vCard',
'add': 'vCard hinzufügen',
'edit': 'bearbeiten',
'saving': 'Speichern…',
'submit': 'Übernehmen',
'cancel': 'Abbruch',
'helpTitle': 'Hilfe zum vCard-Editor',
'submitTitle': 'Änderungen abspeichern',
'cancelTitle': 'Änderungen verwerfen',
'preview': 'Vorschau',
'previewTitle': 'Vorschau der vCard. Bitte vor dem Speichern benutzen!',
'previewOff': 'Vorschau aus',
'previewOffTitle': 'Vorschau ausschalten.',
'refresh': '↺', // \ue031 not yet working
'refreshTitle': 'Vorschau aktualisieren',
'validationEmptyListing': 'Bitte geben Sie entweder einen Namen oder eine Anschrift ein.',
'validationEmail': 'Bitte vergewissern Sie sich, dass die Email fehlerfrei ist.',
'validationImage': 'Bitte geben Sie den Bilddateinamen ohne Präfix ein.',
'image': 'Datei|datei|Bild|bild', //Local prefix for Image (or File)
'added': 'Hinzugefügte vCard für ',
'updated': 'Geänderte vCard für ',
'removed': 'Gelöschte vCard für ',
'mobileEdit': 'Mobile Bearbeitung',
'helpPage': '//de.wikivoyage.org/wiki/Hilfe:vCard-Editor',
'enterCaptcha': 'Eingabe des CAPTCHA',
'externalLinks': 'Ihre Bearbeitung enthält neue externe Links.',
// license text should match MediaWiki:Wikimedia-copyrightwarning
'licenseText': 'Mit dem Speichern von Änderungen erklärst du dich mit den <a class="external" target="_blank" href="//wikimediafoundation.org/wiki/Terms_of_Use/de">Nutzungsbedingungen</a> einverstanden und lizenzierst deine Bearbeitung unwiderruflich unter der Lizenz <a class="external" target="_blank" href="https://creativecommons.org/licenses/by-sa/3.0/deed.de"><i>Creative-Commons</i> „Namensnennung – Weitergabe unter gleichen Bedingungen 3.0“</a> und der <a class="external" target="_blank" href="https://de.wikipedia.org/wiki/Wikipedia:GNU_Free_Documentation_License">GFDL</a>. Du stimmst zu, dass ein Hyperlink oder eine URL zur Seite für die notwendige Zuschreibung gemäß der <i>Creative-Commons</i>-Lizenz ausreichend ist.',
'ajaxInitFailure': 'Fehler: vCard-Editor kann nicht vorausgefüllt werden.',
'sharedName': 'Name',
'sharedImage': 'Bild',
'sharedLatitude': 'Breite',
'sharedLongitude': 'Länge',
'sharedWebsite': 'Website',
'submitApiError': 'Fehler: Der Server lieferte eine Fehlermeldung beim Versuch der Speicherung der vCard. Bitte versuchen Sie es erneut.',
'submitBlacklistError': 'Fehler: Eine Angabe wurde als schwarzgelistet ermittel. Bitte entfernen Sieden Eintrag und versuchen Sie es erneut.',
'submitUnknownError': 'Fehler: Ein unbekannter Fehler ist beim Versuch der Speicherung der vCard aufgetreten. Bitte versuchen Sie es erneut.',
'submitHttpError': 'Fehler: Der Server lieferte einen HTTP-Fehler beim Versuch der Speicherung der vCard. Bitte versuchen Sie es erneut.',
'submitEmptyError': 'Fehler: Der Server lieferte eine leere Antwort beim Versuch der Speicherung der vCard. Bitte versuchen Sie es erneut.',
'viewCommonsPage' : 'Ansicht der Commons-Seite',
'viewWikidataPage' : 'Ansicht des Wikidata-Datensatzes',
'wikidataShared': 'Nachfolgende Daten wurden in der Wikidata-Datensatz gefunden. Sollen die gefundenen Werte übernommen werden?',
'wikidataSharedNotFound': 'Keine Daten in der Wikidata-Datenbank gefunden.'
};
var YES_ARRAY = { 'y': '', 'yes': '', 'j': '', 'ja': '', 'true': '' };
var NO_ARRAY = { 'n': '', 'no': '', 'nein': '', 'false': '' };
var WIKIDATA_CLAIMS = {
'coord': { 'p': 'P625', 'label': 'Koordinate' },
'url': { 'p': 'P856', 'label': 'Website' }, // link
'image': { 'p': 'P18', 'label': 'Bild' },
'address': { 'p': 'P969', 'label': 'Anschrift' },
'phone': { 'p': 'P1329', 'label': 'Telefon' },
'fax': { 'p': 'P2900', 'label': 'Fax' },
'email': { 'p': 'P968', 'label': 'Emails' },
'skype': { 'p': 'P2893', 'label': 'Skype' },
'facebook': { 'p': 'P2013', 'label': 'Facebook' },
'flickr': { 'p': 'P3267', 'label': 'Flickr' },
'google': { 'p': 'P2847', 'label': 'Google+' },
'twitter': { 'p': 'P2002', 'label': 'Twitter' },
'youtube': { 'p': 'P2397', 'label': 'Youtube' }
};
// --------------------------------------------------------------------
// CONFIGURE THE FOLLOWING BASED ON WIKIVOYAGE COMMUNITY PREFERENCES
// --------------------------------------------------------------------
// if the browser window width is less than MAX_DIALOG_WIDTH (pixels), the
// listing editor dialog will fill the available space, otherwise it will
// be limited to the specified width
var MAX_DIALOG_WIDTH = 1200;
// set this flag to false if the listing editor should strip away any
// listing template parameters that are not explicitly configured in the
// LISTING_TEMPLATES parameter arrays (such as wikipedia, phoneextra, etc).
// if the flag is set to true then unrecognized parameters will be allowed
// as long as they have a non-empty value.
var ALLOW_UNRECOGNIZED_PARAMETERS = true;
// --------------------------------------------------------------------
// UPDATE THE FOLLOWING TO MATCH WIKIVOYAGE ARTICLE SECTION NAMES
// --------------------------------------------------------------------
// map section heading ID to the listing template to use for that section
var SECTION_TO_TEMPLATE_TYPE = { // vCard specific
'Anreise': 'go',
'Mobilit.C3.A4t': 'go',
'Sehensw.C3.BCrdigkeiten': 'see',
'Aktivit.C3.A4ten': 'do',
'Einkaufen': 'buy',
'K.C3.BCche': 'eat',
'Nachtleben': 'drink',
'Unterkunft': 'sleep',
'Lernen': 'education',
'Arbeiten': 'administration',
'Sicherheit': 'administration',
'Gesundheit': 'health',
'Praktische_Hinweise': 'other'
};
// 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' ];
// --------------------------------------------------------------------
// CONFIGURE THE FOLLOWING TO MATCH THE LISTING TEMPLATE PARAMS & OUTPUT
// --------------------------------------------------------------------
// name of the generic listing template to use when a more specific
// template ("see", "do", etc) is not appropriate
var DEFAULT_LISTING_TEMPLATE = 'vCard'; // vCard specific
var DEFAULT_TEMPLATE_NO_TYPE = true;
var LISTING_TYPE_PARAMETER = 'type';
var LISTING_CONTENT_PARAMETER = 'description'; // vCard specific
// selector that identifies the HTML elements into which the 'edit' link
// for each listing will be placed
var EDIT_LINK_CONTAINER_SELECTOR = 'span.listing-metadata-items';
// The arrays below must include entries for each listing template
// parameter in use for each Wikivoyage language version - for example
// "name", "address", "phone", etc. If all listing template types use
// the same parameters then a single configuration array is sufficient,
// but if listing templates use different parameters or have different
// rules about which parameters are required then the differences must
// be configured - for example, English Wikivoyage uses "checkin" and
// "checkout" in the "sleep" template, so a separate
// SLEEP_TEMPLATE_PARAMETERS array has been created below to define the
// different requirements for that listing template type.
//
// Once arrays of parameters are defined, the LISTING_TEMPLATES
// mapping is used to link the configuration to the listing template
// type, so in the English Wikivoyage example all listing template
// types use the LISTING_TEMPLATE_PARAMETERS configuration EXCEPT for
// "sleep" listings, which use the SLEEP_TEMPLATE_PARAMETERS
// configuration.
//
// Fields that can used in the configuration array(s):
// - id: HTML input ID in the EDITOR_FORM_HTML for this element.
// - hideDivIfEmpty: id of a <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.
// - skipIfEmpty: Do not include the parameter in the wiki template
// syntax that is saved to the article if the parameter has no
// value. For example, the "image" tag is not included by default
// in the listing template syntax unless it has a value.
// Default skipIfEmpty = true
// - newline: Append a newline after the parameter in the listing
// template syntax when the article is saved.
var SKIP_DEFAULT = true;
var LISTING_TEMPLATE_PARAMETERS = {
'type': { id: 'input-type' },
'name': { id: 'input-name' },
'alt': { id: 'input-alt' },
'comment': { id: 'input-comment' },
'name-local': { id: 'input-name-local' },
'name-latin': { id: 'input-name-latin' },
'address': { id: 'input-address' },
'address-local': { id: 'input-address-local' },
'directions': { id: 'input-directions' },
'directions-local': { id: 'input-directions-local' },
'wikidata': { id: 'input-wikidata-value' },
'auto': { id: 'input-auto' },
'url': { id: 'input-url' },
'facebook': { id: 'input-facebook' },
'google': { id: 'input-google' },
'twitter': { id: 'input-twitter' },
'flickr': { id: 'input-flickr' },
'youtube': { id: 'input-youtube' },
'lat': { id: 'input-lat' },
'long': { id: 'input-long' },
'image': { id: 'input-image' },
'group': { id: 'input-group' },
'intl-area-code': { id: 'input-intl-area-code' },
'phone': { id: 'input-phone' },
'tollfree': { id: 'input-tollfree' },
'mobile': { id: 'input-mobile' },
'fax': { id: 'input-fax' },
'email': { id: 'input-email' },
'skype': { id: 'input-skype' },
'hours': { id: 'input-hours' },
'checkin': { id: 'input-checkin' },
'checkout': { id: 'input-checkout' },
'price': { id: 'input-price' },
'lastedit': { id: 'input-lastedit', hideDivIfEmpty: 'div_lastedit' },
'credit-cards': { id: 'input-credit-cards' },
'subtype': { id: 'input-subtype' },
'show': { id: 'input-show' },
'before': { id: 'input-before' },
'description': { id: 'input-content', skipIfEmpty: false }
};
// Type dependent hide /show
var HIDE_AND_SHOW = {
'sleep': {
hide: ['div_hours'],
show: ['div_checkin', 'div_checkout']
},
'default': {
hide: ['div_checkin', 'div_checkout'],
show: ['div_hours']
}
};
// map the template name to configuration information needed by the listing
// editor
var LISTING_TEMPLATES = { // vCard specific
'listing': '',
'see': '',
'do': '',
'buy': '',
'eat': '',
'drink': '',
'sleep': '',
'vCard': ''
};
// --------------------------------------------------------------------
// CONFIGURE THE FOLLOWING TO IMPLEMENT THE UI FOR THE LISTING EDITOR
// --------------------------------------------------------------------
// these selectors should match a value defined in the EDITOR_FORM_HTML
// if the selector refers to a field that is not used by a Wikivoyage
// language version the variable should still be defined, but the
// corresponding element in EDITOR_FORM_HTML can be removed and thus
// the selector will not match anything and the functionality tied to
// the selector will never execute.
var EDITOR_FORM_SELECTOR = '#listing-editor';
var EDITOR_CLOSED_SELECTOR = '#input-closed';
var EDITOR_SUMMARY_SELECTOR = '#input-summary';
var EDITOR_MINOR_EDIT_SELECTOR = '#input-minor';
// --------------------------------------------------------------------
// ADDING TYPE OPTIONS and DEFAULT_PLACEHOLDERS
// --------------------------------------------------------------------
var DEFAULT_PLACEHOLDERS = [
{ p: ' Bezeichnung der Einrichtung', s: 'input-name' },
{ p: ' Auch bekannt als', s: 'input-alt' },
{ p: ' Beispiel: http://www.beispiel.de', s: 'input-url' },
{ p: ' Anschrift der Einrichtung', s: 'input-address' },
{ p: ' Angaben zur Lage der Einrichtung', s: 'input-directions' },
{ p: ' Beispiel: +55', s: 'input-intl-area-code' },
{ p: ' Beispiel: +55 555 555-5555', s: 'input-phone' },
{ p: ' Beispiel: +49 800 100-1000', s: 'input-tollfree' },
{ p: ' Beispiel: +55 123 555-555', s: 'input-mobile' },
{ p: ' Beispiel: +55 555 555-555', s: 'input-fax' },
{ p: ' Beispiel: hallo@beispiel.de', s: 'input-email' },
{ p: ' Hinweis zur Bezeichnung', s: 'input-comment' },
{ p: ' Beispiel: myskype', s: 'input-skype' },
{ p: ' Beispiele: myfacebook, https://www.facebook.com/myfacebook', s: 'input-facebook' },
{ p: ' Beispiel: myflickr', s: 'input-flickr' },
{ p: ' Beispiel: mytwitter', s: 'input-twitter' },
{ p: ' Beispiel: myyoutube', s: 'input-youtube' },
{ p: ' Beispiel: Master, Visa, Amex', s: 'input-credit-cards' },
{ p: ' Beispiel: 2015-01-15', s: 'input-lastedit' },
{ p: 'Type der Einrichtung', s: 'input-type' },
{ p: 'Alternative Typ-Gruppe', s: 'input-group' },
{ p: ' Wikidata-Bezeichnung', s: 'input-wikidata-label' },
{ p: 'Automatischer Bezug aus Wikidata', s: 'input-auto' },
{ p: ' Beispiel: 11.11111', s: 'input-lat' },
{ p: ' Beispiel: 111.11111', s: 'input-long' },
{ p: ' Beispiel: Mo-Fr 9-18 Uhr', s: 'input-hours' },
{ p: ' Checkin-Zeit', s: 'input-checkin' },
{ p: ' Checkout-Zeit', s: 'input-checkout' },
{ p: ' Eintritts- oder Dienstleistungspreis', s: 'input-price' },
{ p: ' Abbildung der Einrichtung', s: 'input-image' },
{ p: 'Automatischer Bezug aus Wikidata' },
{ p: 'Untergruppen', s: 'input-subtype' },
{ p: ' Beispiel: المتحف المصري', s: 'input-name-local' },
{ p: ' Beispiel: al-Matḥaf al-Miṣrī', s: 'input-name-latin' },
{ p: ' Beispiel: ميدان التحرير', s: 'input-address-local' },
{ p: ' Beispiel: بوسط البلد', s: 'input-directions-local' },
{ p: ' Beispiel: [[Datei:Sternchen.jpg]]', s: 'input-before' },
];
var GROUP_PROPERTIES = [
{ group: 'do', label: 'Aktivität', color: '#808080' }, // grey
{ group: 'other', label: 'Anderes', color: '#228B22' }, // forestgreen
{ group: 'go', label: 'Anreise', color: '#A52A2A' }, // brown
{ group: 'view', label: 'Aussicht', color: '#4169E1' }, // royalblue
{ group: 'buy', label: 'Einkaufen', color: '#008080' }, // teal
{ group: 'eat', label: 'Essen', color: '#D2691E' }, // chocolate
{ group: 'vicinity', label: 'Nahe Umgebung', color: '#800000' }, // maroon
{ group: 'see', label: 'Sehenswürdigkeit', color: '#4682B4' }, // steelblue
{ group: 'populated', label: 'Bewohntes Gebiet', color: '#0000FF' }, // blue
{ group: 'drink', label: 'Trinken', color: '#000000' }, //black
{ group: 'around', label: 'Umgebung', color: '#800080' }, // purple
{ group: 'sleep', label: 'Unterkunft', color: '#000080' }, // dark blue
{ group: 'target', label: 'Ziel', color: '#FFCCCC' }, //pink
{ group: 'mediumaquamarine', label: 'Aquamarinblau', color: '#66CDAA' },
{ group: 'blue', label: 'Blau', color: '#0000FF' },
{ group: 'teal', label: 'Blaugrün', color: '#008080' },
{ group: 'brown', label: 'Braun', color: '#A52A2A' },
{ group: 'gold', label: 'Gold', color: '#FFD700' },
{ group: 'grey', label: 'Grau', color: '#808080' },
{ group: 'lime', label: 'Hellgrün', color: '#00FF00' },
{ group: 'maroon', label: 'Kastanienbraun', color: '#800000' },
{ group: 'royalblue', label: 'Königsblau', color: '#4169E1' },
{ group: 'magenta', label: 'Magentarot', color: '#FF00FF' },
{ group: 'navy', label: 'Marineblau', color: '#000080' },
{ group: 'orange', label: 'Orange', color: '#FFA500' },
{ group: 'plum', label: 'Pflaumenblau', color: '#DDA0DD' },
{ group: 'fuchsia', label: 'Purpurrot', color: '#FF00FF' },
{ group: 'red', label: 'Rot', color: '#FF0000' },
{ group: 'chocolate', label: 'Schokobraun', color: '#D2691E' },
{ group: 'black', label: 'Schwarz', color: '#000000' },
{ group: 'silver', label: 'Silber', color: '#C0C0C0' },
{ group: 'steelblue', label: 'Stahlblau', color: '#4682B4' },
{ group: 'purple', label: 'Violett', color: '#800080' },
{ group: 'forestgreen', label: 'Waldgrün', color: '#228B22' }
];
var LISTING_TYPE_OPTIONS = [
// journey and mobility
{ 'type': 'aerialway', group: 'go', label: 'Seilbahn' },
{ 'type': 'airline', group: 'go', label: 'Fluggesellschaft' },
{ 'type': 'airport', group: 'go', label: 'Flughafen' },
{ 'type': 'airport_service', group: 'go', label: 'Flughafendienste' },
{ 'type': 'bicycle_rental', group: 'go', label: 'Fahrradverleih' },
{ 'type': 'bicycle_parking', group: 'go', label: 'Fahrradparkplatz' },
{ 'type': 'boat_rental', group: 'go', label: 'Bootsverleih' },
{ 'type': 'boat_sharing', group: 'go', label: 'Boot-Sharing/Charter' },
{ 'type': 'border_control', group: 'go', label: 'Grenzkontrolle' },
{ 'type': 'bus', group: 'go', label: 'Bus' },
{ 'type': 'car_rental', group: 'go', label: 'Autoverleih' },
{ 'type': 'car_repair', group: 'go', label: 'Autowerkstatt' },
{ 'type': 'car_sharing', group: 'go', label: 'Car Sharing' },
{ 'type': 'ferry', group: 'go', label: 'Fähre' },
{ 'type': 'fuel', group: 'go', label: 'Tankstelle' },
{ 'type': 'marina', group: 'go', label: 'Yachthafen' },
{ 'type': 'motorbike_rental', group: 'go', label: 'Motorradverleih' },
{ 'type': 'parking', group: 'go', label: 'Parkplatz' },
{ 'type': 'port', group: 'go', label: 'Hafen' },
{ 'type': 'public_transport', group: 'go', label: 'Öffentliche Verkehrsmittel' },
{ 'type': 'shipping_company', group: 'go', label: 'Schiffahrtsgesellschaft' },
{ 'type': 'shuttle_bus_service', group: 'go', label: 'Shuttlebus-Service' },
{ 'type': 'station', group: 'go', label: 'Station' },
{ 'type': 'subway', group: 'go', label: 'U-Bahn' },
{ 'type': 'taxi', group: 'go', label: 'Taxistand' },
{ 'type': 'terminal', group: 'go', label: 'Terminal' },
{ 'type': 'train', group: 'go', label: 'Bahnhof, Haltepunkt' },
{ 'type': 'tram', group: 'go', label: 'Straßenbahn' },
{ 'type': 'waypoint', group: 'go', label: 'Wegpunkt' },
{ 'type': 'go', group: 'go', label: 'Anreise' },
// sights
{ 'type': 'aquarium', group: 'see', label: 'Aquarium' },
{ 'type': 'archaeological_site', group: 'see', label: 'Archäologische Stätte' },
{ 'type': 'artwork', group: 'see', label: 'Kunstwerk' },
{ 'type': 'bay', group: 'see', label: 'Bucht' },
{ 'type': 'botanical_garden', group: 'see', label: 'Botanischer Garten' },
{ 'type': 'bridge', group: 'see', label: 'Brücke' },
{ 'type': 'building', group: 'see', label: 'Gebäude' },
{ 'type': 'bunker', group: 'see', label: 'Bunker' },
{ 'type': 'canal', group: 'see', label: 'Kanal' },
{ 'type': 'castle', group: 'see', label: 'Schloss' },
{ 'type': 'cathedral', group: 'see', label: 'Kathedrale' },
{ 'type': 'cave', group: 'see', label: 'Höhle' },
{ 'type': 'cemetery', group: 'see', label: 'Friedhof' },
{ 'type': 'church', group: 'see', label: 'Kirche' },
{ 'type': 'crocodile_farm', group: 'see', label: 'Krokodilfarm' },
{ 'type': 'farm', group: 'see', label: 'Bauernhof' },
{ 'type': 'forest', group: 'see', label: 'Wald' },
{ 'type': 'fort', group: 'see', label: 'Festung' },
{ 'type': 'gallery', group: 'see', label: 'Galerie' },
{ 'type': 'garden', group: 'see', label: 'Garten' },
{ 'type': 'gate', group: 'see', label: 'Tor' },
{ 'type': 'habitat', group: 'see', label: 'Habitat' },
{ 'type': 'island', group: 'see', label: 'Insel' },
{ 'type': 'lake', group: 'see', label: 'See' },
{ 'type': 'landmark', group: 'see', label: 'Wahrzeichen' },
{ 'type': 'landscape', group: 'see', label: 'Landschaft' },
{ 'type': 'lighthouse', group: 'see', label: 'Leuchtturm' },
{ 'type': 'mausoleum', group: 'see', label: 'Mausoleum' },
{ 'type': 'memorial', group: 'see', label: 'Denkmal' },
{ 'type': 'mill', group: 'see', label: 'Mühle' },
{ 'type': 'mine', group: 'see', label: 'Bergwerk' }, // including surface mining
{ 'type': 'mining_museum', group: 'see', label: 'Bergbaumuseum' },
{ 'type': 'monastery', group: 'see', label: 'Kloster' },
{ 'type': 'monument', group: 'see', label: 'Monument' },
{ 'type': 'mosque', group: 'see', label: 'Moschee' },
{ 'type': 'museum', group: 'see', label: 'Museum' },
{ 'type': 'national_park', group: 'see', label: 'Nationalpark' },
{ 'type': 'nature_reserve', group: 'see', label: 'Naturschutzgebiet' },
{ 'type': 'nunnery', group: 'see', label: 'Nonnenkloster' },
{ 'type': 'observatory', group: 'see', label: 'Sternwarte' },
{ 'type': 'open_air_museum', group: 'see', label: 'Freilichtmuseum' },
{ 'type': 'pagoda', group: 'see', label: 'Pagode' },
{ 'type': 'palace', group: 'see', label: 'Palast' },
{ 'type': 'palaeontological_site', group: 'see', label: 'Paläontologische Stätte' },
{ 'type': 'park', group: 'see', label: 'Park' },
{ 'type': 'pedestrian', group: 'see', label: 'Fußgängerzone' },
{ 'type': 'peninsula', group: 'see', label: 'Halbinsel' },
{ 'type': 'planetarium', group: 'see', label: 'Planetarium' },
{ 'type': 'plantation', group: 'see', label: 'Plantage' },
{ 'type': 'power', group: 'see', label: 'Kraftwerk, Energie' }, // power station, transformers, towers etc.
{ 'type': 'pyramid', group: 'see', label: 'Pyramide' },
{ 'type': 'quarry', group: 'see', label: 'Steinbruch' },
{ 'type': 'road', group: 'see', label: 'Straße' }, // including street and lane
{ 'type': 'religious_site', group: 'see', label: 'Religiöse Stätte' },
{ 'type': 'spring', group: 'see', label: 'Quelle' },
{ 'type': 'square', group: 'see', label: 'Platz' },
{ 'type': 'stud', group: 'see', label: 'Gestüt' },
{ 'type': 'synagogue', group: 'see', label: 'Synagoge' },
{ 'type': 'temple', group: 'see', label: 'Tempel' },
{ 'type': 'tomb', group: 'see', label: 'Grab' },
{ 'type': 'tower', group: 'see', label: 'Turm' },
{ 'type': 'town_hall', group: 'see', label: 'Rathaus' },
{ 'type': 'tumulus', group: 'see', label: 'Tumulus' },
{ 'type': 'viewpoint', group: 'see', label: 'Aussichtspunkt' },
{ 'type': 'war_grave', group: 'see', label: 'Kriegsgrab' },
{ 'type': 'waterfall', group: 'see', label: 'Wasserfall' },
{ 'type': 'well', group: 'see', label: 'Brunnen' },
{ 'type': 'wetland', group: 'see', label: 'Feuchtgebiet' },
{ 'type': 'zoo', group: 'see', label: 'Zoo, Tierpark' },
{ 'type': 'see', group: 'see', label: 'Sehenswürdigkeit' },
// culture
{ 'type': 'arts_centre', group: 'do', label: 'Kulturzentrum' },
{ 'type': 'cabaret', group: 'do', label: 'Cabaret' },
{ 'type': 'cinema', group: 'do', label: 'Kino' },
{ 'type': 'circus', group: 'do', label: 'Zirkus' },
{ 'type': 'edutainment', group: 'do', label: 'Edutainment' },
{ 'type': 'fair', group: 'do', label: 'Messe' },
{ 'type': 'festival', group: 'do', label: 'Festspiele' },
{ 'type': 'music', group: 'do', label: 'Musikdarbietung' }, // different types of music theaters
{ 'type': 'opera_house', group: 'do', label: 'Opernhaus' },
{ 'type': 'puppet_theater', group: 'do', label: 'Puppentheater' },
{ 'type': 'theater', group: 'do', label: 'Theater' }, // speech theater, theater in general
// recreation
{ 'type': 'amusement_park', group: 'do', label: 'Vergnügungspark' },
{ 'type': 'attraction', group: 'do', label: 'Attraktion' },
{ 'type': 'ballooning', group: 'do', label: 'Ballonfahren' },
{ 'type': 'casino', group: 'do', label: 'Spielbank, Casino' },
{ 'type': 'dolphinarium', group: 'do', label: 'Delfinarium' },
{ 'type': 'entertainment', group: 'do', label: 'Unterhaltung' },
{ 'type': 'horsecar', group: 'do', label: 'Pferdewagen' },
{ 'type': 'massage', group: 'do', label: 'Massagesalon' },
{ 'type': 'playground', group: 'do', label: 'Spielplatz' },
{ 'type': 'ropes_course', group: 'do', label: 'Seilgarten' },
{ 'type': 'sauna', group: 'do', label: 'Sauna' },
{ 'type': 'spa', group: 'do', label: 'Heilbad' },
{ 'type': 'theme_park', group: 'do', label: 'Themenpark' },
// sports and fitness
{ 'type': '9pin', group: 'do', label: 'Kegeln' },
{ 'type': '10pin', group: 'do', label: 'Bowling' },
{ 'type': 'badminton', group: 'do', label: 'Badminton' },
{ 'type': 'beach', group: 'do', label: 'Strand' },
{ 'type': 'beachvolleyball', group: 'do', label: 'Beachvolleyball' },
{ 'type': 'billiards', group: 'do', label: 'Billiard' },
{ 'type': 'bmx', group: 'do', label: 'BMX' },
{ 'type': 'bobsleigh', group: 'do', label: 'Bobsport' },
{ 'type': 'boules', group: 'do', label: 'Boules' },
{ 'type': 'bungee_jumping', group: 'do', label: 'Bungeespringen' },
{ 'type': 'canoe', group: 'do', label: 'Bootssport' }, // Kanu, Kayak
{ 'type': 'canyoning', group: 'do', label: 'Canyoning' },
{ 'type': 'climbing', group: 'do', label: 'Bergsteigen' },
{ 'type': 'cricket', group: 'do', label: 'Cricket' },
{ 'type': 'dive_center', group: 'do', label: 'Tauchzentrum' },
{ 'type': 'downhill', group: 'do', label: 'Schi Alpin, Abfahrt' },
{ 'type': 'equestrian', group: 'do', label: 'Reitsport' },
{ 'type': 'extreme_sports', group: 'do', label: 'Extremsport' },
{ 'type': 'fishing', group: 'do', label: 'Fischen, Angeln' },
{ 'type': 'fitness_centre', group: 'do', label: 'Fitnessstudio' }, // including gym
{ 'type': 'gliding', group: 'do', label: 'Segelflug' },
{ 'type': 'golf', group: 'do', label: 'Golf' },
{ 'type': 'hiking', group: 'do', label: 'Wandern' }, // including trekking
{ 'type': 'hillwalking', group: 'do', label: 'Bergwandern' },
{ 'type': 'horse_racing', group: 'do', label: 'Pferderennen' },
{ 'type': 'hunting', group: 'do', label: 'Jagd' },
{ 'type': 'ice_hockey', group: 'do', label: 'Eishockey' },
{ 'type': 'ice_skating', group: 'do', label: 'Eislauf' },
{ 'type': 'karting', group: 'do', label: 'Kartsport' },
{ 'type': 'kitesurfing', group: 'do', label: 'Kitesurfen' },
{ 'type': 'minigolf', group: 'do', label: 'Minigolf' },
{ 'type': 'motocross', group: 'do', label: 'Motocross' },
{ 'type': 'mountain', group: 'do', label: 'Berg' },
{ 'type': 'nordic', group: 'do', label: 'Schi Nordisch' }, // nordic/cross country ski trail
{ 'type': 'orienteering', group: 'do', label: 'Orientierungslauf' },
{ 'type': 'parachuting', group: 'do', label: 'Fallschirmspringen' }, // including skydiving
{ 'type': 'paragliding', group: 'do', label: 'Paragliding' },
{ 'type': 'polo', group: 'do', label: 'Polo' },
{ 'type': 'quad_bike', group: 'do', label: 'Quad-Bike' },
{ 'type': 'racetrack', group: 'do', label: 'Rennbahn' },
{ 'type': 'rafting', group: 'do', label: 'Rafting' }, // including whitewater rafting
{ 'type': 'roller_skating', group: 'do', label: 'Rollsport' }, // including inline roller skating
{ 'type': 'rowing', group: 'do', label: 'Rudern' },
{ 'type': 'sailing', group: 'do', label: 'Segeln' },
{ 'type': 'sandboarding', group: 'do', label: 'Sandboarding' },
{ 'type': 'scuba_diving', group: 'do', label: 'Tauchen' },
{ 'type': 'ski_jumping', group: 'do', label: 'Schi-Springen' },
{ 'type': 'ski_school', group: 'do', label: 'Schischule' },
{ 'type': 'skitour', group: 'do', label: 'Schi-Touren' },
{ 'type': 'sled', group: 'do', label: 'Schlittensport' },
{ 'type': 'snow_park', group: 'do', label: 'Snowpark' },
{ 'type': 'soccer', group: 'do', label: 'Fußball' },
{ 'type': 'sports', group: 'do', label: 'Sport' },
{ 'type': 'stadium', group: 'do', label: 'Stadion' },
{ 'type': 'summit', group: 'do', label: 'Gipfel' },
{ 'type': 'surfing', group: 'do', label: 'Surfen' },
{ 'type': 'swimming', group: 'do', label: 'Schwimmen' },
{ 'type': 'swimming_pool', group: 'do', label: 'Schwimmbad' },
{ 'type': 'tennis', group: 'do', label: 'Tennis' },
{ 'type': 'triathlon', group: 'do', label: 'Triathlon' },
{ 'type': 'wakeboarding', group: 'do', label: 'Wakeboarding' },
{ 'type': 'water_ski', group: 'do', label: 'Wasserski' },
{ 'type': 'water_sports', group: 'do', label: 'Wassersport' },
{ 'type': 'winter_sports', group: 'do', label: 'Wintersport' },
{ 'type': 'do', group: 'do', label: 'Aktivität' },
// shopping
{ 'type': 'antiquarian', group: 'buy', label: 'Antiquariat' },
{ 'type': 'atm', group: 'buy', label: 'Geldautomat' },
{ 'type': 'bakery', group: 'buy', label: 'Bäckerei' },
{ 'type': 'bank', group: 'buy', label: 'Bank, Geldwesen' },
{ 'type': 'beverages', group: 'buy', label: 'Getränkegeschäft' },
{ 'type': 'bike_shop', group: 'buy', label: 'Fahrradladen' },
{ 'type': 'book_seller', group: 'buy', label: 'Buchladen' },
{ 'type': 'boutique', group: 'buy', label: 'Boutique' },
{ 'type': 'butchery', group: 'buy', label: 'Metzgerei, Fleischerei' },
{ 'type': 'carpet_shop', group: 'buy', label: 'Teppichgeschäft' },
{ 'type': 'chemist', group: 'buy', label: 'Drogerie' },
{ 'type': 'collector_shop', group: 'buy', label: 'Sammlergeschäft' },
{ 'type': 'cosmetics_shop', group: 'buy', label: 'Kosmetik' },
{ 'type': 'crafts_shop', group: 'buy', label: 'Handwerksgeschäft' },
{ 'type': 'delicatessen', group: 'buy', label: 'Spezialitätengeschäft' },
{ 'type': 'department_store', group: 'buy', label: 'Kaufhaus' },
{ 'type': 'duty-free_shop', group: 'buy', label: 'Duty-Free-Geschäft' },
{ 'type': 'fashion_store', group: 'buy', label: 'Modegeschäft' },
{ 'type': 'flea_market', group: 'buy', label: 'Flohmarkt' },
{ 'type': 'hairdresser', group: 'buy', label: 'Friseur' },
{ 'type': 'jewellery', group: 'buy', label: 'Juwelier' },
{ 'type': 'kiosk', group: 'buy', label: 'Kiosk' },
{ 'type': 'mall', group: 'buy', label: 'Einkaufszentrum, Mall' },
{ 'type': 'market', group: 'buy', label: 'Markt' },
{ 'type': 'music_shop', group: 'buy', label: 'Musikgeschäft' },
{ 'type': 'optician', group: 'buy', label: 'Optiker' },
{ 'type': 'outdoor_retailer', group: 'buy', label: 'Outdoor-Händler' },
{ 'type': 'pastry_shop', group: 'buy', label: 'Konditorei' },
{ 'type': 'photo_store', group: 'buy', label: 'Fotogeschäft' },
{ 'type': 'second_hand', group: 'buy', label: 'Gebrauchtwarenhändler' },
{ 'type': 'shop', group: 'buy', label: 'Geschäft' },
{ 'type': 'souvenir_shop', group: 'buy', label: 'Souvenirgeschäft' },
{ 'type': 'sports_shop', group: 'buy', label: 'Sportgeschäft' },
{ 'type': 'supermarket', group: 'buy', label: 'Supermarkt' },
{ 'type': 'watersports_shop', group: 'buy', label: 'Wassersportgeschäft' },
{ 'type': 'buy', group: 'buy', label: 'Einkaufen' },
{ 'type': 'bistro', group: 'eat', label: 'Bistro' },
{ 'type': 'brasserie', group: 'eat', label: 'Brasserie' },
{ 'type': 'brewery', group: 'eat', label: 'Brauerei' },
{ 'type': 'cafe', group: 'eat', label: 'Café' },
{ 'type': 'cafeteria', group: 'eat', label: 'Cafeteria' },
{ 'type': 'canteen', group: 'eat', label: 'Kantine' }, // auch für Mensa
{ 'type': 'coffee_shop', group: 'eat', label: 'Kaffeegeschäft' },
{ 'type': 'fast_food', group: 'eat', label: 'Fastfood' },
{ 'type': 'ice_cream', group: 'eat', label: 'Eisdiele' },
{ 'type': 'restaurant', group: 'eat', label: 'Restaurant' },
{ 'type': 'restaurant_and_bar', group: 'eat', label: 'Restaurant und Bar' },
{ 'type': 'snack_bar', group: 'eat', label: 'Imbiss' },
{ 'type': 'steak_house', group: 'eat', label: 'Steakhaus' },
{ 'type': 'eat', group: 'eat', label: 'Küche, Essen' },
{ 'type': 'bar', group: 'drink', label: 'Bar' },
{ 'type': 'beer_garden', group: 'drink', label: 'Biergarten' },
{ 'type': 'club', group: 'drink', label: 'Club' },
{ 'type': 'discotheque', group: 'drink', label: 'Diskothek' },
{ 'type': 'distillery', group: 'drink', label: 'Brennerei' },
{ 'type': 'nightclub', group: 'drink', label: 'Nachtclub' },
{ 'type': 'pub', group: 'drink', label: 'Kneipe' },
{ 'type': 'vineyard', group: 'drink', label: 'Weingut' },
{ 'type': 'drink', group: 'drink', label: 'Trinken' },
{ 'type': 'appartment', group: 'sleep', label: 'Appartements' },
{ 'type': 'alpine_hut', group: 'sleep', label: 'Berghütte' },
{ 'type': 'bed_and_bike', group: 'sleep', label: 'Bett und Bike' },
{ 'type': 'boarding_house', group: 'sleep', label: 'Pension' },
{ 'type': 'campsite', group: 'sleep', label: 'Campingplatz' },
{ 'type': 'caravan_site', group: 'sleep', label: 'Caravan-Campingplatz' },
{ 'type': 'chalet', group: 'sleep', label: 'Chalet' },
{ 'type': 'guest_house', group: 'sleep', label: 'Gästehaus' },
{ 'type': 'holiday_flat', group: 'sleep', label: 'Ferienwohnung' },
{ 'type': 'hostel', group: 'sleep', label: 'Herberge' },
{ 'type': 'hotel', group: 'sleep', label: 'Hotel' },
{ 'type': 'hotel_garni', group: 'sleep', label: 'Hotel Garni' },
{ 'type': 'motel', group: 'sleep', label: 'Motel' },
{ 'type': 'resort', group: 'sleep', label: 'Ressort' },
{ 'type': 'wilderness_hut', group: 'sleep', label: 'Wildnishütte' },
{ 'type': 'youth_hostel', group: 'sleep', label: 'Jugendherberge' },
{ 'type': 'sleep', group: 'sleep', label: 'Unterkunft' },
// health
{ 'type': 'clinic', group: 'other', label: 'Klinik' },
{ 'type': 'dentist', group: 'other', label: 'Zahnarzt' },
{ 'type': 'health', group: 'other', label: 'Gesundheit' },
{ 'type': 'health_centre', group: 'other', label: 'Ärztehaus' },
{ 'type': 'hospital', group: 'other', label: 'Krankenhaus' },
{ 'type': 'laboratory', group: 'other', label: 'Labor' },
{ 'type': 'naturopathy', group: 'other', label: 'Naturheilkunde' },
{ 'type': 'nursing_home', group: 'other', label: 'Pflegeheim' },
{ 'type': 'pharmacy', group: 'other', label: 'Apotheke' },
{ 'type': 'practice', group: 'other', label: 'Praxis' },
{ 'type': 'rehabilitation', group: 'other', label: 'Rehabilitation' },
{ 'type': 'surgery', group: 'other', label: 'Arztpraxis' },
{ 'type': 'veterinary', group: 'other', label: 'Tierarzt' },
// teaching & learning
{ 'type': 'academy', group: 'other', label: 'Akademie' },
{ 'type': 'bookcase', group: 'other', label: 'Bücherschrank' },
{ 'type': 'college', group: 'other', label: 'Hochschule' },
{ 'type': 'cooking_class', group: 'other', label: 'Kochkurse' },
{ 'type': 'education', group: 'other', label: 'Ausbildung' },
{ 'type': 'kindergarten', group: 'other', label: 'Kindergarten' },
{ 'type': 'language_school', group: 'other', label: 'Sprachschule' },
{ 'type': 'library', group: 'other', label: 'Bücherei' },
{ 'type': 'nursery', group: 'other', label: 'Kinderkrippe' },
{ 'type': 'school', group: 'other', label: 'Schule' },
{ 'type': 'university', group: 'other', label: 'Universität' },
// administration and uncategorized
{ 'type': 'administration', group: 'other', label: 'Verwaltung' },
{ 'type': 'company', group: 'other', label: 'Unternehmen' },
{ 'type': 'consulate', group: 'other', label: 'Konsulat' },
{ 'type': 'customs', group: 'other', label: 'Zoll' },
{ 'type': 'cultural_organisation', group: 'other', label: 'Kulturorganisation' },
{ 'type': 'embassy', group: 'other', label: 'Botschaft' },
{ 'type': 'fire_brigade', group: 'other', label: 'Feuerwehr' },
{ 'type': 'government', group: 'other', label: 'Regierungsgebäude' },
{ 'type': 'guide', group: 'other', label: 'Fremdenführer' },
{ 'type': 'internet_cafe', group: 'other', label: 'Internetcafé' },
{ 'type': 'laundry', group: 'other', label: 'Wäscherei' },
{ 'type': 'listing', group: 'other', label: 'Listung' },
{ 'type': 'lost_and_found', group: 'other', label: 'Fundbüro' },
{ 'type': 'mobile_telephony', group: 'other', label: 'Mobilfunkgeschäft' },
{ 'type': 'office', group: 'other', label: 'Büro' },
{ 'type': 'organisation', group: 'other', label: 'Organisation' },
{ 'type': 'phone', group: 'other', label: 'Telefon' },
{ 'type': 'police', group: 'other', label: 'Polizei' },
{ 'type': 'post', group: 'other', label: 'Post' },
{ 'type': 'relief_organisation', group: 'other', label: 'Wohltätigkeitsorganisation' },
{ 'type': 'shelter', group: 'other', label: 'Unterstand' },
{ 'type': 'toilet', group: 'other', label: 'Toiletten' },
{ 'type': 'tour_operator', group: 'other', label: 'Ausflugsveranstalter' },
{ 'type': 'tourism_authority', group: 'other', label: 'Tourismusbehörde' },
{ 'type': 'tourist_information', group: 'other', label: 'Touristinformation' },
{ 'type': 'travel_agency', group: 'other', label: 'Reisebüro' },
{ 'type': 'other', group: 'other', label: 'Anderes' },
// settlements
{ 'type': 'city', group: 'populated', label: 'Stadt' },
{ 'type': 'holiday_resort', group: 'populated', label: 'Feriensiedlung' },
{ 'type': 'municipality', group: 'populated', label: 'Gemeinde' },
{ 'type': 'quarter', group: 'populated', label: 'Stadtteil' },
{ 'type': 'settlement', group: 'populated', label: 'Siedlung' },
{ 'type': 'town', group: 'populated', label: 'Kleinstadt' },
{ 'type': 'village', group: 'populated', label: 'Dorf' },
// view
{ 'type': 'scenic_view', group: 'view', label: 'Bildmotiv' },
];
LISTING_TYPE_OPTIONS.sort( function(a, b) {
return a.label.localeCompare(b.label);
});
// the below HTML is the UI that will be loaded into the listing editor
// dialog box when a listing is added or edited. EACH WIKIVOYAGE
// LANGUAGE SITE CAN CUSTOMIZE THIS HTML - fields can be removed,
// added, displayed differently, etc. Note that it is important that
// any changes to the HTML structure are also made to the
// LISTING_TEMPLATES parameter arrays since that array provides the
// mapping between the editor HTML and the listing template fields.
var EDITOR_FORM_HTML = '<form id="listing-editor">' +
'<div class="listing-container">' +
'<div class="listing-col listing-span_1_of_2">' +
'<div id="div_name" class="editor-row">' +
'<div><label for="input-name" title="Bezeichnung der Einrichtung">Name</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Bezeichnung der Einrichtung" id="input-name"></div>' +
'</div>' +
'<div id="div_alt" class="editor-row">' +
'<div><label for="input-alt" title="Alternative aktuelle Bezeichnung der Einrichtung">Alternative</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Auch bekannt als" id="input-alt"></div>' +
'</div>' +
'<div id="div_comment" class="editor-row">' +
'<div><label for="input-comment" title="Anmerkung zum Namen oder zur Einrichtung, die nicht Namensbestandteil ist. Zum Beispiel frühere Namen.">Kommentar</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Hinweis zur Bezeichnung" id="input-comment"></div>' +
'</div>' +
'<div id="div_url" class="editor-row">' +
'<div><label for="input-url" class="wikidata-update" title="Webadresse der Einrichtung">Website</label></div>' +
'<div><input type="url" class="editor-fullwidth" placeholder=" Beispiel: http://www.beispiel.de" id="input-url"></div>' +
'</div>' +
'<div id="div_address" class="editor-row">' +
'<div><label for="input-address" class="wikidata-update" title="Anschrift der Einrichtung">Anschrift</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Anschrift der Einrichtung" id="input-address"></div>' +
'</div>' +
'<div id="div_directions" class="editor-row">' +
'<div><label for="input-directions" title="Angaben zur Lage der Einrichtung">Lage</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Angaben zur Lage der Einrichtung" id="input-directions"></div>' +
'</div>' +
'<div id="div_intl-area-code" class="editor-row">' +
'<div><label for="input-intl-area-code" title="Ländervorwahl für alle Telefonnummern">Intl. Vorwahl</label></div>' +
'<div><input type="text" class="editor-partialwidth" placeholder=" Beispiel: +55" id="input-intl-area-code"></div>' +
'</div>' +
'<div id="div_phone" class="editor-row">' +
'<div><label for="input-phone" class="wikidata-update" title="Telefonnummern der Einrichtung, mit Komma getrennt">Telefon</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: +55 555 555-5555" id="input-phone"></div>' +
'</div>' +
'<div id="div_tollfree" class="editor-row">' +
'<div><label for="input-tollfree" title="Gebührenfreie Telefonnummern der Einrichtung, mit Komma getrennt">Gebührenfrei</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: +49 800 100-1000" id="input-tollfree"></div>' +
'</div>' +
'<div id="div_mobile" class="editor-row">' +
'<div><label for="input-mobile" title="Mobil-Telefonnummern der Einrichtung, mit Komma getrennt">Handy</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: +55 123 555-555" id="input-mobile"></div>' +
'</div>' +
'<div id="div_fax" class="editor-row">' +
'<div><label for="input-fax" class="wikidata-update" title="Fax-Telefonnummern der Einrichtung, mit Komma getrennt">Fax</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: +55 555 555-555" id="input-fax"></div>' +
'</div>' +
'<div id="div_email" class="editor-row">' +
'<div><label for="input-email" class="wikidata-update" title="E-Mail-Adressen der Einrichtung, mit Komma getrennt">E-Mails</label></div>' +
'<div><input type="email" multiple="" class="editor-fullwidth" placeholder=" Beispiel: hallo@beispiel.de" id="input-email"></div>' +
'</div>' +
'<div id="div_skype" class="editor-row">' +
'<div><label for="input-skype" class="wikidata-update" title="Skype-Benutzername der Einrichtung">Skype-Name</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: myskype" id="input-skype"></div>' +
'</div>' +
'<div id="div_facebook" class="editor-row">' +
'<div><label for="input-facebook" class="wikidata-update" title="Facebook-Webadresse oder Facebook-Profil-ID der Einrichtung">Facebook-URL</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiele: myfacebook, https://www.facebook.com/myfacebook" id="input-facebook"></div>' +
'</div>' +
'<div id="div_google" class="editor-row">' +
'<div><label for="input-google" class="wikidata-update" title="Google+-Webadresse der Einrichtung">Google+-URL</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: mygoogle" id="input-google"></div>' +
'</div>' +
'<div id="twitter" class="editor-row">' +
'<div><label for="input-twitter" class="wikidata-update" title="Twitter-Webadresse der Einrichtung">Twitter-URL</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: mytwitter" id="input-twitter"></div>' +
'</div>' +
'<div id="div_flickr" class="editor-row">' +
'<div><label for="input-flickr" class="wikidata-update" title="Name der flickr-Gruppe">flickr-Gruppe</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: myflickr" id="input-flickr"></div>' +
'</div>' +
'<div id="youtube" class="editor-row">' +
'<div><label for="input-youtube" class="wikidata-update" title="Webadresse des YouTube-Kanals">YouTube-Kanal</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: myyoutube" id="input-youtube"></div>' +
'</div>' +
'</div>' +
'<div class="listing-col listing-span_1_of_2">' +
'<div id="div_type" class="editor-row">' +
'<div><label for="input-type" class="wikidata-update" title="Typ der Einrichtung">Typ</label></div>' +
'<div>' +
'<select id="input-type" placeholder="Type der Einrichtung">' +
'<option value=""></option>';
var anOptionObj;
for (var i = 0; i < LISTING_TYPE_OPTIONS.length; i++) {
anOptionObj = LISTING_TYPE_OPTIONS[i];
EDITOR_FORM_HTML += '<option value="' + anOptionObj.type + '">' + anOptionObj.label + '</option>';
}
EDITOR_FORM_HTML +=
'</select>' +
' <label for="input-group" title="Ü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)">Typ-Gruppe</label> ' +
'<select id="input-group" placeholder="Alternative Typ-Gruppe">' +
'<option value=""></option>';
for (i = 0; i < GROUP_PROPERTIES.length; i++) {
anOptionObj = GROUP_PROPERTIES[i];
EDITOR_FORM_HTML += '<option value="' + anOptionObj.group + '">' + anOptionObj.label + '</option>';
}
EDITOR_FORM_HTML +=
'</select>' +
'</div>' +
'</div>' +
'<div id="div_wikidata" class="editor-row">' +
'<div><label for="input-wikidata-label" title="Bezeichnung des Wikidata-Datenobjekts">Wikidata</label></div>' +
'<div>' +
'<input type="text" class="editor-partialwidth" placeholder=" Wikidata-Bezeichnung" id="input-wikidata-label">' +
'<input type="hidden" id="input-wikidata-value">' +
'<span id="wikidata-value-display-container" style="display:none">' +
'<small>' +
' <span id="wikidata-value-link"></span>' +
' | <a href="javascript:" id="wikidata-remove" title="Lösche Wikidata-Eintrag aus der vCard">löschen</a>' +
'</small>' +
'</span>' +
'</div>' +
'</div>' +
'<div id="div_auto" class="editor-row" style="display: none">' +
'<div><label for="input-auto" title="Automatischer Bezug aller Angaben aus Wikidata">Auto</label></div>' +
'<div><select id="input-auto" placeholder="Automatischer Bezug aus Wikidata">' +
'<option value=""></option>' +
'<option value="y">ja</option>' +
'<option value="n">nein</option>' +
'</select> <span id="div_wikidata_update" style="display: none"><a href="javascript:" id="wikidata-shared">Aktualisiere Felder aus Wikidata</a></span></div>' +
'</div>' +
'<div id="div_lat" class="editor-row">' +
'<div><label for="input-lat" class="wikidata-update" title="Geografische Breite der Position der Einrichtung">Breite</label></div>' +
'<div><input type="text" class="editor-partialwidth" placeholder=" Beispiel: 11.11111" id="input-lat">' +
// update the ListingEditor.Callbacks.initFindOnMapLink
// method if this field is removed or modified
' <a id="geomap-link" target="_blank" href="http://maps.wikivoyage-ev.org/w/geomap.php">Suche auf einer Karte</a></div>' +
'</div>' +
'<div id="div_long" class="editor-row">' +
'<div><label for="input-long" class="wikidata-update" title="Geografische Länge der Position der Einrichtung">Länge</label></div>' +
'<div><input type="text" class="editor-partialwidth" placeholder=" Beispiel: 111.11111" id="input-long"></div>' +
'</div>' +
'<div id="div_hours" class="editor-row">' +
'<div><label for="input-hours" title="Öffnungszeiten der Einrichtung">Geöffnet</label></div>' +
'<div><input type="text" size="255" class="editor-fullwidth" placeholder=" Beispiel: Mo-Fr 9-18 Uhr" id="input-hours"></div>' +
'</div>' +
'<div id="div_checkin" class="editor-row">' +
'<div><label for="input-checkin" title="Früheste Checkin-Zeit">Checkin</label></div>' +
'<div><input type="text" size="255" class="editor-fullwidth" placeholder=" Checkin-Zeit" id="input-checkin"></div>' +
'</div>' +
'<div id="div_checkout" class="editor-row">' +
'<div><label for="input-checkout" title="Späteste Checkout-Zeit">Checkout</label></div>' +
'<div><input type="text" size="255" class="editor-fullwidth" placeholder=" Checkout-Zeit" id="input-checkout"></div>' +
'</div>' +
'<div id="div_price" class="editor-row">' +
'<div><label for="input-price" title="Eintritts- oder Übernachtungspreise der Einrichtung">Preis</label></div>' +
'<div>' +
// update the ListingEditor.Callbacks.initCurrencySymbolFormFields
// method if the currency symbols are removed or modified
'<input type="text" class="editor-partialwidth" placeholder=" Eintritts- oder Dienstleistungspreis" id="input-price">' +
'<span id="span_currency">' +
'<span class="currency-signs"> <a href="javascript:">\u20AC</a></span>' +
'<span class="currency-signs"> <a href="javascript:">\u0024</a></span>' +
'<span class="currency-signs"> <a href="javascript:">\u00A3</a></span>' +
'<span class="currency-signs"> <a href="javascript:">\u00A5</a></span>' +
'<span class="currency-signs"> <a href="javascript:">\u20A9</a></span>' +
'</span>' +
'</div>' +
'</div>' +
'<div id="credit-cards" class="editor-row">' +
'<div><label for="input-credit-cards" title="Von der Einrichtung akzeptierte Kreditkarten">Kreditkarten</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: Master, Visa, Amex" id="input-credit-cards"></div>' +
'</div>' +
'<div id="div_image" class="editor-row">' +
'<div><label for="input-image" class="wikidata-update" title="Bild, das beim Anklicken der Nummer auf der Karte eingeblendet werden soll">Bild</label></div>' +
'<div>' +
'<input type="text" class="editor-partialwidth" placeholder=" Abbildung der Einrichtung" id="input-image">' +
'<span id="image-value-display-container" style="display:none">' +
'<small>' +
' <span id="image-value-link"></span>' +
'</small>' +
'</span>' +
'</div>' +
'</div>' +
'<div id="div_show" class="editor-row">' +
'<div><label for="input-show" title="Legt die Anzeige von POIs und Koordinaten fest. Nur nötig, wenn der Anzeigemodus vom Standard (meist „nur Poi“) abweicht">Anzeigemodus</label></div>' +
'<div><select id="input-show" placeholder="Automatischer Bezug aus Wikidata">' +
'<option value=""></option>' +
'<option value="all">POI und Koordinaten</option>' +
'<option value="poi">nur POI</option>' +
'<option value="coord">nur Koordinaten</option>' +
'<option value="none">gar nichts</option>' +
'</select></div>' +
'</div>' +
'<div id="div_subtype" class="editor-row">' +
'<div><label for="input-subtype" title="Preislage der Einrichtung">Untertyp</label></div>' +
'<div><select id="input-subtype" placeholder="Untergruppen">' +
'<option value=""></option>' +
'<option value="budget">günstig</option>' +
'<option value="midrange">mittel</option>' +
'<option value="upmarket">gehoben</option>' +
'</select></div>' +
'</div>' +
'<div id="div_name-local" class="editor-row">' +
'<div><label for="input-name-local" class="wikidata-update" title="Bezeichnung der Einrichtung in der Landessprache. Zusätzlich zu Name">Name in Landessprache</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: المتحف المصري" id="input-name-local"></div>' +
'</div>' +
'<div id="div_name-latin" class="editor-row">' +
'<div><label for="input-name-latin" title="Bezeichnung der Einrichtung in der Umschrift der Landessprache">Name in Umschrift</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: al-Matḥaf al-Miṣrī" id="input-name-latin"></div>' +
'</div>' +
'<div id="div_address-local" class="editor-row">' +
'<div><label for="input-address-local" class="wikidata-update" title="Anschrift der Einrichtung in der Landessprache. Zusätzlich zur Anschrift">Anschrift in Landessprache</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: ميدان التحرير" id="input-address-local"></div>' +
'</div>' +
'<div id="div_directions-local" class="editor-row">' +
'<div><label for="input-directions-local" title="Angaben zur Lage der Einrichtung in der Landessprache. Zusätzlich zur Lage">Lage in Landessprache</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: بوسط البلد" id="input-directions-local"></div>' +
'</div>' +
'<div id="div_before" class="editor-row">' +
'<div><label for="input-before" title="An den Anfang gestellte Symbole usw.">Voranstellung</label></div>' +
'<div><input type="text" class="editor-fullwidth" placeholder=" Beispiel: [[Datei:Sternchen.jpg]]" id="input-before"></div>' +
'</div>' +
'</div>' +
'</div>' +
'<div id="div_content" class="editor-row">' +
'<div><label for="input-content" title="Beschreibung der Einrichtung">Beschreibung</label></div>' +
'<div><textarea rows="8" class="editor-fullwidth" placeholder="Beschreibung der Einrichtung" id="input-content"></textarea></div>' +
'</div>' +
// update the ListingEditor.Callbacks.hideEditOnlyFields method if
// the status row is removed or modified
'<div id="div_status" class="editor-row">' +
'<div><label title="Angaben zum Artikelstatus wie Löschung oder Aktualisierung">Status</label></div>' +
'<div>' +
'<span id="span-closed">' +
'<input type="checkbox" id="input-closed">' +
'<label for="input-closed" class="listing-tooltip" title="Markiere diese Option, falls diese Einrichtung nicht mehr besteht oder falls der Eintrag aus einem anderen Grund gelöscht werden soll. Der Eintrag wird dann aus dem Artikel entfernt.">Diesen Eintrag löschen?</label>' +
'</span>' +
// update the ListingEditor.Callbacks.updateLastEditDate
// method if the last edit input is removed or modified
'<span id="div_lastedit">' +
'<label for="input-lastedit" title="Datum der letzten Bearbeitung in der Form jjjj-mm-tt, sog. ISO-Datum. Bei leer gelassenem Feld wird beim Abspeichern das heutige Datum eingetragen.">Letzte Aktualisierung</label> ' +
'<input type="text" size="10" placeholder="2015-01-15" id="input-lastedit">' +
'</span>' +
'<span id="span-last-edit">' +
'<input type="checkbox" id="input-last-edit" />' +
'<label for="input-last-edit" class="listing-tooltip" title="Markiere diese Option, falls die Angaben als aktuell und fehlerfrei überprüft wurden. Als Update-Datum wird der Eintrag oder das Datum des heutigen Tags verwendet.">Diesen Eintrag als aktuell markieren?</label>' +
'</span>' +
'</div>' +
'</div>' +
// update the ListingEditor.Callbacks.hideEditOnlyFields method if
// the summary table is removed or modified
'<div id="div_summary">'+
'<div class="listing-divider"></div>' +
'<div class="editor-row">' +
'<div><label for="input-summary" title="Kurze Zusammenfassung zum Grund der Änderung">Zusammenfassung</label></div>' +
'<div>' +
'<input type="text" class="editor-partialwidth" placeholder="Grund der Änderung der vCard" id="input-summary">' +
'<span id="span-minor"><input type="checkbox" id="input-minor"><label for="input-minor" class="listing-tooltip" title="Markiere diese Option, falls nur geringfügige Änderungen wie z. B. Schreibfehler ausgeführt wurden.">Nur Kleinigkeiten wurden verändert</label></span>' +
'</div>' +
'</div>' +
'</div>' +
'<div id="listing-preview" style="display: none;">' +
'<div class="listing-divider"></div>' +
'<div class="editor-row">' +
'<div title="Vorschau der vCard mit den aktuellen Formulardaten">Vorschau</div>' +
'<div><div id="listing-preview-text"></div></div>' +
'</div>' +
'</div>' +
'</form>';
// expose public members
return {
LANG: LANG,
COMMONS_URL: COMMONS_URL,
WIKIDATA_URL: WIKIDATA_URL,
WIKIPEDIA_URL: WIKIPEDIA_URL,
WIKIDATA_SITELINK_WIKIPEDIA: WIKIDATA_SITELINK_WIKIPEDIA,
WIKIDATA_CLAIMS: WIKIDATA_CLAIMS,
TRANSLATIONS: TRANSLATIONS,
MAX_DIALOG_WIDTH: MAX_DIALOG_WIDTH,
DISALLOW_ADD_LISTING_IF_PRESENT: DISALLOW_ADD_LISTING_IF_PRESENT,
DEFAULT_LISTING_TEMPLATE: DEFAULT_LISTING_TEMPLATE,
DEFAULT_TEMPLATE_NO_TYPE: DEFAULT_TEMPLATE_NO_TYPE,
LISTING_TYPE_PARAMETER: LISTING_TYPE_PARAMETER,
LISTING_CONTENT_PARAMETER: LISTING_CONTENT_PARAMETER,
DEFAULT_PLACEHOLDERS: DEFAULT_PLACEHOLDERS,
HIDE_AND_SHOW: HIDE_AND_SHOW,
EDIT_LINK_CONTAINER_SELECTOR: EDIT_LINK_CONTAINER_SELECTOR,
ALLOW_UNRECOGNIZED_PARAMETERS: ALLOW_UNRECOGNIZED_PARAMETERS,
SECTION_TO_TEMPLATE_TYPE: SECTION_TO_TEMPLATE_TYPE,
LISTING_TEMPLATES: LISTING_TEMPLATES,
LISTING_TEMPLATE_PARAMETERS: LISTING_TEMPLATE_PARAMETERS,
EDITOR_FORM_SELECTOR: EDITOR_FORM_SELECTOR,
EDITOR_CLOSED_SELECTOR: EDITOR_CLOSED_SELECTOR,
EDITOR_SUMMARY_SELECTOR: EDITOR_SUMMARY_SELECTOR,
EDITOR_MINOR_EDIT_SELECTOR: EDITOR_MINOR_EDIT_SELECTOR,
LISTING_TYPE_OPTIONS: LISTING_TYPE_OPTIONS,
GROUP_PROPERTIES: GROUP_PROPERTIES,
EDITOR_FORM_HTML: EDITOR_FORM_HTML,
YES_ARRAY: YES_ARRAY,
NO_ARRAY: NO_ARRAY
};
}();
// -------------------------- ListingEditor.Callbacks--------------------------
/* ***********************************************************************
* ListingEditor.Callbacks implements custom functionality that may be
* specific to how a Wikivoyage language version has implemented the
* listing template. For example, English Wikivoyage uses a "last edit"
* date that needs to be populated when the listing editor form is
* submitted, and that is done via custom functionality implemented as a
* SUBMIT_FORM_CALLBACK function in this module.
* ***********************************************************************/
ListingEditor.Callbacks = function() {
// array of functions to invoke when creating the listing editor form.
// these functions will be invoked with the form DOM object as the
// first element and the mode as the second element.
var CREATE_FORM_CALLBACKS = [];
// array of functions to invoke when submitting the listing editor
// form but prior to validating the form. these functions will be
// invoked with the mapping of listing attribute to value as the first
// element and the mode as the second element.
var SUBMIT_FORM_CALLBACKS = [];
// array of validation functions to invoke when the listing editor is
// submitted. these functions will be invoked with an array of
// validation messages as an argument; a failed validation should add a
// message to this array, and the user will be shown the messages and
// the form will not be submitted if the array is not empty.
var VALIDATE_FORM_CALLBACKS = [];
// --------------------------------------------------------------------
// LISTING EDITOR UI INITIALIZATION CALLBACKS
// --------------------------------------------------------------------
/**
* Add listeners to the currency symbols so that clicking on a currency
* symbol will insert it into the price input.
*/
var initCurrencySymbolFormFields = function(form, mode) {
var CURRENCY_SIGNS_SELECTOR = '.currency-signs';
$(CURRENCY_SIGNS_SELECTOR, form).click(function() {
var priceInput = $('#input-price');
var caretPos = priceInput[0].selectionStart;
var oldPrice = priceInput.val();
var currencySymbol = $(this).find('a').text();
var newPrice = oldPrice.substring(0, caretPos) + currencySymbol + oldPrice.substring(caretPos);
priceInput.val(newPrice);
priceInput.select();
// now setting the cursor behind the currency symbol inserted
priceInput[0].setSelectionRange(caretPos + 1, caretPos + 1);
});
};
CREATE_FORM_CALLBACKS.push(initCurrencySymbolFormFields);
/**
* Add listeners on various fields to update the "find on map" link.
*/
var initFindOnMapLink = function(form, mode) {
var latlngStr = '?lang=' + ListingEditor.Config.LANG;
latlngStr += '&page=' + encodeURIComponent(mw.config.get('wgTitle'));
// #geodata should be a hidden span added by Template:Geo
// containing the lat/long coordinates of the destination
if ($('#geodata').length) {
var latlng = $('#geodata').text().split('; ');
latlngStr += '&lat=' + latlng[0] + '&lon=' + latlng[1] + '&zoom=15';
}
if ($('#input-address', form).val() !== '') {
latlngStr += '&location=' + encodeURIComponent($('#input-address', form).val());
} else if ($('#input-name', form).val() !== '') {
latlngStr += '&location=' + encodeURIComponent($('#input-name', form).val());
}
// #geomap-link is a link in the EDITOR_FORM_HTML
$('#geomap-link', form).attr('href', $('#geomap-link', form).attr('href') + latlngStr);
$('#input-address', form).change( function () {
var link = $('#geomap-link').attr('href');
var index = link.indexOf('&location');
if (index < 0) index = link.length;
$('#geomap-link').attr('href', link.substr(0,index) + '&location='
+ encodeURIComponent($('#input-address').val()));
});
};
CREATE_FORM_CALLBACKS.push(initFindOnMapLink);
/**
* Add listeners on type selector field.
*/
var showAndHideFields = function(valueSelected, form) {
var group = '';
var anOptionObj;
var i;
for (i = 0; i < ListingEditor.Config.LISTING_TYPE_OPTIONS.length; i++) {
anOptionObj = ListingEditor.Config.LISTING_TYPE_OPTIONS[i];
if (anOptionObj.type === valueSelected) {
group = anOptionObj.group;
break;
}
}
anOptionObj = ListingEditor.Config.HIDE_AND_SHOW[group];
if (!anOptionObj) anOptionObj = ListingEditor.Config.HIDE_AND_SHOW.default;
if (anOptionObj && anOptionObj.show.length > 0) {
for(i = 0; i < anOptionObj.show.length; i++) {
$('#' + anOptionObj.show[i], form).show();
}
}
if (anOptionObj && anOptionObj.hide.length > 0) {
for(i = 0; i < anOptionObj.hide.length; i++) {
if ($('#' + anOptionObj.hide[i] + ' input', form).val() === '')
$('#' + anOptionObj.hide[i], form).hide();
}
}
// set input shadow
var color = '#ffffff';
for (i = 0; i < ListingEditor.Config.GROUP_PROPERTIES.length; i++) {
anOptionObj = ListingEditor.Config.GROUP_PROPERTIES[i];
if (anOptionObj.group === group) {
color = anOptionObj.color;
break;
}
}
$('#input-type', form).css( 'box-shadow', '20px 0 0 0 ' + color + ' inset' );
};
var initTypeSelector = function(form, mode) {
showAndHideFields( $('#input-type', form).val(), form );
$('#input-type', form).change(function () {
var optionSelected = $('option:selected', this);
var valueSelected = this.value;
showAndHideFields(valueSelected, form);
});
};
CREATE_FORM_CALLBACKS.push(initTypeSelector);
var setGroupColor = function(group, form) {
var anOptionObj;
var i;
var color = '#ffffff';
for (i = 0; i < ListingEditor.Config.GROUP_PROPERTIES.length; i++) {
anOptionObj = ListingEditor.Config.GROUP_PROPERTIES[i];
if (anOptionObj.group === group) {
color = anOptionObj.color;
break;
}
}
$('#input-group', form).css( 'box-shadow', '20px 0 0 0 ' + color + ' inset' );
};
var initGroupSelector = function(form, mode) {
setGroupColor( $('#input-group', form).val(), form );
$('#input-group', form).on('keydown keyup change', function () {
/* var optionSelected = $('option:selected', this);
var valueSelected = this.value;
setGroupColor(valueSelected, form); */
var box = $(this);
setTimeout(function() {
setGroupColor(box.val(), form);
}, 0);
});
};
CREATE_FORM_CALLBACKS.push(initGroupSelector);
var initLastEditCheckBox = function(form, mode) {
$('#input-last-edit', form).change(function () {
if (this.checked && $('#div_lastedit', form).is(':visible'))
$('#input-lastedit', form).val( currentLastEditDate() );
});
};
CREATE_FORM_CALLBACKS.push(initLastEditCheckBox);
var hideEditOnlyFields = function(form, mode) {
var EDITOR_STATUS_ROW = '#div_status';
var EDITOR_SUMMARY_ROW = '#div_summary';
if (mode !== ListingEditor.Core.MODE_EDIT) {
$(EDITOR_STATUS_ROW, form).hide();
$(EDITOR_SUMMARY_ROW, form).hide();
}
};
CREATE_FORM_CALLBACKS.push(hideEditOnlyFields);
var checkForSplit = function(value, form) {
if ($('#input-long', form).val() !== '') return;
value = value.toUpperCase();
var coords = value.split(/[,;]/);
if (coords.length === 2) {
$('#input-lat', form).val( coords[0].trim() );
$('#input-long', form).val( coords[1].trim() );
return;
}
var dir = ['N', 'S'];
for (var i = 0; i < dir.length; i++ ) {
coords = value.split(dir[i]);
if (coords.length === 2) {
$('#input-lat', form).val( coords[0].trim() + ' ' + dir[i]);
$('#input-long', form).val( coords[1].trim() );
return;
}
}
};
var splitCoordinate = function(form, mode) {
checkForSplit( $('#input-lat', form).val(), form );
$('#input-lat', form).blur(function() {
checkForSplit( $('#input-lat', form).val(), form );
});
};
CREATE_FORM_CALLBACKS.push(splitCoordinate);
var addInputClass = function(form) {
$('input', form).each( function() {
if ($( this ).val() === '') $( this ).addClass( "listing-empty-input" );
else $( this ).removeClass( "listing-empty-input" );
});
};
var checkEmptyInput = function (form, mode) {
addInputClass(form);
$('input', form).blur(function() { addInputClass( form ); });
};
CREATE_FORM_CALLBACKS.push(checkEmptyInput);
var setDefaultPlaceholders = function() {
var obj;
for (var i = 0; i < ListingEditor.Config.DEFAULT_PLACEHOLDERS.length; i++) {
obj = ListingEditor.Config.DEFAULT_PLACEHOLDERS[i];
$("#" + obj.s).attr('placeholder', obj.p);
$("#" + obj.s).addClass('listing-default-placeholder');
$("#" + obj.s).removeClass('listing-wikidata-placeholder');
}
};
var updatePlaceholder = function(selector, value) {
if (value) {
$(selector).attr('placeholder', value);
$(selector).addClass('listing-wikidata-placeholder');
$(selector).removeClass('listing-default-placeholder');
}
};
var updatePlaceholders = function(value) {
setDefaultPlaceholders();
var ajaxUrl = ListingEditor.SisterSite.API_WIKIDATA;
var ajaxData = {
action: 'wbgetentities',
ids: value,
languages: ListingEditor.Config.LANG,
};
var ajaxSuccess = function (jsonObj) {
var res;
for (var key in ListingEditor.Config.WIKIDATA_CLAIMS) {
res = ListingEditor.SisterSite.wikidataClaim(jsonObj, value, ListingEditor.Config.WIKIDATA_CLAIMS[key].p);
if (res) {
if (key === 'coord') {
res.latitude = ListingEditor.Core.trimDecimal(res.latitude, 6);
res.longitude = ListingEditor.Core.trimDecimal(res.longitude, 6);
updatePlaceholder('#input-lat', res.latitude);
updatePlaceholder('#input-long', res.longitude);
}
else if (key === 'email') {
res = res.replace('mailto:', '');
updatePlaceholder('#input-' + key, res);
}
else updatePlaceholder('#input-' + key, res);
}
res = ListingEditor.SisterSite.wikidataLabel(jsonObj, value);
updatePlaceholder('#input-wikidata-label', res);
$('#input-wikidata-label').removeClass('listing-wikidata-placeholder');
$('#input-wikidata-label').addClass('listing-default-placeholder');
updatePlaceholder('#input-name', res);
}
};
ListingEditor.SisterSite.ajaxSisterSiteSearch(ajaxUrl, ajaxData, ajaxSuccess);
};
var wikidataLookup = function(form, mode) {
// get the display value for the pre-existing wikidata record ID
var value = $("#input-wikidata-value", form).val();
if (value) {
wikidataLink(form, value);
var ajaxUrl = ListingEditor.SisterSite.API_WIKIDATA;
var ajaxData = {
action: 'wbgetentities',
ids: value,
languages: ListingEditor.Config.LANG,
props: 'labels'
};
var ajaxSuccess = function(jsonObj) {
var value = $("#input-wikidata-value").val();
var label = ListingEditor.SisterSite.wikidataLabel(jsonObj, value);
if (label === null) {
label = "";
}
if (label === '') $("#input-wikidata-label").addClass(' listing-empty-input');
else $("#input-wikidata-label").removeClass(' listing-empty-input');
$("#input-wikidata-label").val(label);
};
ListingEditor.SisterSite.ajaxSisterSiteSearch(ajaxUrl, ajaxData, ajaxSuccess);
updatePlaceholders(value);
}
// set up autocomplete to search for results as the user types
/* $('#input-wikidata-label', form).autocomplete({
source: function( request, response ) {
var ajaxUrl = ListingEditor.SisterSite.API_WIKIDATA;
var ajaxData = {
action: 'wbsearchentities',
search: request.term,
language: ListingEditor.Config.LANG
};
var ajaxSuccess = function (jsonObj) {
response(parseWikiDataResult(jsonObj));
};
ListingEditor.SisterSite.ajaxSisterSiteSearch(ajaxUrl, ajaxData, ajaxSuccess);
},
select: function(event, ui) {
$("#input-wikidata-value").val(ui.item.id);
wikidataLink("", ui.item.id);
updatePlaceholders(ui.item.id);
}
}).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() {
wikidataRemove(form);
});
$('#input-wikidata-label', form).change(function() {
if (!$(this).val()) {
wikidataRemove(form);
}
});
var wikidataRemove = function(form) {
$("#input-wikidata-value", form).val("");
$("#input-wikidata-label", form).val("");
$("#wikidata-value-display-container", form).hide();
$('#div_wikidata_update', form).hide();
$('#div_auto', form).hide();
$('#input-auto').val(''); // vCard specific
setDefaultPlaceholders();
};
$('#wikidata-shared', form).click(function() {
var wikidataRecord = $("#input-wikidata-value", form).val();
updateWikidataSharedFields(wikidataRecord);
});
var commonsSiteData = {
apiUrl: ListingEditor.SisterSite.API_COMMONS,
selector: $('#input-image', form),
form: form,
ajaxData: {
namespace: 6
},
updateLinkFunction: commonsLink
};
ListingEditor.SisterSite.initializeSisterSiteAutocomplete(commonsSiteData);
};
var commonsLink = function(value, form) {
var commonsSiteLinkData = {
inputSelector: '#input-image',
containerSelector: '#image-value-display-container',
linkContainerSelector: '#image-value-link',
href: ListingEditor.Config.COMMONS_URL + '/wiki/' + mw.util.wikiUrlencode('File:' + value),
linkTitle: ListingEditor.Config.TRANSLATIONS.viewCommonsPage
};
sisterSiteLinkDisplay(commonsSiteLinkData, form);
};
var sisterSiteLinkDisplay = function(siteLinkData, form) {
var value = $(siteLinkData.inputSelector, form).val();
if (!value) {
$(siteLinkData.containerSelector, form).hide();
} else {
var link = $("<a />", {
target: "_new",
href: siteLinkData.href,
title: siteLinkData.linkTitle,
text: siteLinkData.linkTitle
});
$(siteLinkData.linkContainerSelector, form).html(link);
$(siteLinkData.containerSelector, form).show();
}
};
var updateFieldIfNotNull = function(selector, value) {
if (value) {
$(selector).val(value);
$(selector).removeClass('listing-empty-input');
}
};
var updateWikidataSharedFields = function(wikidataRecord) {
var ajaxUrl = ListingEditor.SisterSite.API_WIKIDATA;
var ajaxData = {
action: 'wbgetentities',
ids: wikidataRecord,
languages: ListingEditor.Config.LANG,
};
var ajaxSuccess = function (jsonObj) {
var msg = '';
var label = ListingEditor.SisterSite.wikidataLabel(jsonObj, wikidataRecord);
if (label !== null) msg += '\n' + ListingEditor.Config.TRANSLATIONS.sharedName + ': ' + label;
var res = {};
for (var key in ListingEditor.Config.WIKIDATA_CLAIMS) {
res[key] = ListingEditor.SisterSite.wikidataClaim(jsonObj, wikidataRecord, ListingEditor.Config.WIKIDATA_CLAIMS[key].p);
if (res[key]) {
if (key === 'coord') {
res[key].latitude = ListingEditor.Core.trimDecimal(res[key].latitude, 6);
res[key].longitude = ListingEditor.Core.trimDecimal(res[key].longitude, 6);
msg += '\n' + ListingEditor.Config.TRANSLATIONS.sharedLatitude + ': ' + res[key].latitude;
msg += '\n' + ListingEditor.Config.TRANSLATIONS.sharedLongitude + ': ' + res[key].longitude;
}
else if (key === 'email') {
res[key] = res[key].replace('mailto:', '');
msg += '\n' + ListingEditor.Config.WIKIDATA_CLAIMS[key].label + ': ' + res[key];
}
else msg += '\n' + ListingEditor.Config.WIKIDATA_CLAIMS[key].label + ': ' + res[key];
}
}
if (msg) {
if (confirm(ListingEditor.Config.TRANSLATIONS.wikidataShared + '\n' + msg)) {
if (label !== null) updateFieldIfNotNull('#input-name', label);
for (key in res) {
if (res[key]) {
if (key === 'coord') {
updateFieldIfNotNull('#input-lat', res[key].latitude);
updateFieldIfNotNull('#input-long', res[key].longitude);
}
else if (key === 'image') {
updateFieldIfNotNull('#input-image', res[key]);
commonsLink(res[key]);
}
else {
updateFieldIfNotNull('#input-' + key, res[key]);
}
}
$('#input-auto').val(''); // vCard specific
}
}
} else {
alert(ListingEditor.Config.TRANSLATIONS.wikidataSharedNotFound);
}
};
ListingEditor.SisterSite.ajaxSisterSiteSearch(ajaxUrl, ajaxData, ajaxSuccess);
};
var parseWikiDataResult = function(jsonObj) {
var results = [];
for (var i=0; i < $(jsonObj.search).length; i++) {
var result = $(jsonObj.search)[i];
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) {
var link = $("<a />", {
target: "_new",
href: ListingEditor.Config.WIKIDATA_URL + '/wiki/' + mw.util.wikiUrlencode(value),
title: ListingEditor.Config.TRANSLATIONS.viewWikidataPage,
text: value
});
$("#wikidata-value-link", form).html(link);
$("#wikidata-value-display-container", form).show();
$('#input-auto').val('y'); // vCard specific
$('#div_wikidata_update', form).show();
$('#div_auto', form).show();
};
CREATE_FORM_CALLBACKS.push(wikidataLookup);
// --------------------------------------------------------------------
// LISTING EDITOR FORM SUBMISSION CALLBACKS
// --------------------------------------------------------------------
/**
* Return the current date in the format "2015-01-15".
*/
var currentLastEditDate = function() {
var d = new Date();
var year = d.getFullYear();
// Date.getMonth() returns 0-11
var month = d.getMonth() + 1;
if (month < 10) month = '0' + month;
var day = d.getDate();
if (day < 10) day = '0' + day;
return year + '-' + month + '-' + day;
};
/**
* Only update last edit date if this is a new listing or if the
* "information up-to-date" box checked.
*/
var updateLastEditDate = function(listing, mode) {
var LISTING_LAST_EDIT_PARAMETER = 'lastedit';
var EDITOR_LAST_EDIT_SELECTOR = '#input-last-edit';
if (mode == ListingEditor.Core.MODE_ADD || $(EDITOR_LAST_EDIT_SELECTOR).is(':checked')) {
listing[LISTING_LAST_EDIT_PARAMETER] = currentLastEditDate();
}
};
SUBMIT_FORM_CALLBACKS.push(updateLastEditDate);
// --------------------------------------------------------------------
// LISTING EDITOR FORM VALIDATION CALLBACKS
// --------------------------------------------------------------------
/**
* Verify all listings have at least a name, address or alt value.
*/
var validateListingHasData = function(validationFailureMessages) {
if ($('#input-name').val() === '' && $('#input-address').val() === ''
&& $('#input-alt').val() === '' && $('#input-wikidata-value').val() === '') {
validationFailureMessages.push(ListingEditor.Config.TRANSLATIONS.validationEmptyListing);
}
};
VALIDATE_FORM_CALLBACKS.push(validateListingHasData);
/**
* Implement SIMPLE validation on email addresses. Invalid emails can
* still get through, but this method implements a minimal amount of
* validation in order to catch the worst offenders.
*/
var validateEmail = function(validationFailureMessages) {
var VALID_EMAIL_REGEX = /^([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+,*[\W]*)+$/;
_validateFieldAgainstRegex(validationFailureMessages, VALID_EMAIL_REGEX, '#input-email', ListingEditor.Config.TRANSLATIONS.validationEmail);
};
VALIDATE_FORM_CALLBACKS.push(validateEmail);
/**
* Implement SIMPLE validation on the Commons field to verify that the
* user has not included a "File" or "Image" namespace.
*/
var validateImage = function(validationFailureMessages) {
var VALID_IMAGE_REGEX = new RegExp('^(?!(file|image|' + ListingEditor.Config.TRANSLATIONS.image + '):)', 'i');
_validateFieldAgainstRegex(validationFailureMessages, VALID_IMAGE_REGEX, '#input-image', ListingEditor.Config.TRANSLATIONS.validationImage);
};
VALIDATE_FORM_CALLBACKS.push(validateImage);
var _validateFieldAgainstRegex = function(validationFailureMessages, validationRegex, fieldPattern, failureMsg) {
var fieldValue = $(fieldPattern).val().trim();
if (fieldValue !== '' && !validationRegex.test(fieldValue)) {
validationFailureMessages.push(failureMsg);
}
};
// expose public members
return {
CREATE_FORM_CALLBACKS: CREATE_FORM_CALLBACKS,
SUBMIT_FORM_CALLBACKS: SUBMIT_FORM_CALLBACKS,
VALIDATE_FORM_CALLBACKS: VALIDATE_FORM_CALLBACKS
};
}();
ListingEditor.SisterSite = function() {
var API_WIKIDATA = ListingEditor.Config.WIKIDATA_URL + '/w/api.php';
var API_WIKIPEDIA = ListingEditor.Config.WIKIPEDIA_URL + '/w/api.php';
var API_COMMONS = ListingEditor.Config.COMMONS_URL + '/w/api.php';
var _initializeSisterSiteAutocomplete = function(siteData) {
var currentValue = $(siteData.selector).val();
if (currentValue) {
siteData.updateLinkFunction(currentValue, siteData.form);
}
$(siteData.selector).change(function() {
siteData.updateLinkFunction($(siteData.selector).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 = [];
var titleResults = $(jsonObj[1]);
for (var i=0; i < titleResults.length; i++) {
var result = titleResults[i];
var valueWithoutFileNamespace = (titleResults[i].indexOf("File:") != -1) ? titleResults[i].substring("File:".length) : titleResults[i];
var titleResult = { value: valueWithoutFileNamespace, label: titleResults[i], description: $(jsonObj[2])[i], link: $(jsonObj[3])[i] };
results.push(titleResult);
}
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);
};
// perform an ajax query of a sister site
var _ajaxSisterSiteSearch = function(ajaxUrl, ajaxData, ajaxSuccess) {
ajaxData.format = 'json';
$.ajax({
url: ajaxUrl,
data: ajaxData,
dataType: 'jsonp',
success: ajaxSuccess
});
};
// parse the wikidata "claim" object from the wikidata response
var _wikidataClaim = function(jsonObj, value, property) {
var entity = _wikidataEntity(jsonObj, value);
if (!entity || !entity.claims || !entity.claims[property]) {
return null;
}
var propertyObj = entity.claims[property];
if (!propertyObj || propertyObj.length < 1 || !propertyObj[0].mainsnak || !propertyObj[0].mainsnak.datavalue) {
return null;
}
return propertyObj[0].mainsnak.datavalue.value;
};
// parse the wikidata "entity" object from the wikidata response
var _wikidataEntity = function(jsonObj, value) {
if (!jsonObj || !jsonObj.entities || !jsonObj.entities[value]) {
return null;
}
return jsonObj.entities[value];
};
// parse the wikidata display label from the wikidata response
var _wikidataLabel = function(jsonObj, value) {
var entityObj = _wikidataEntity(jsonObj, value);
if (!entityObj || !entityObj.labels || !entityObj.labels[ListingEditor.Config.LANG]) { /* mod */
return null;
}
return entityObj.labels[ListingEditor.Config.LANG].value;
};
// expose public members
return {
API_WIKIDATA: API_WIKIDATA,
API_WIKIPEDIA: API_WIKIPEDIA,
API_COMMONS: API_COMMONS,
initializeSisterSiteAutocomplete: _initializeSisterSiteAutocomplete,
ajaxSisterSiteSearch: _ajaxSisterSiteSearch,
wikidataClaim: _wikidataClaim,
wikidataLabel: _wikidataLabel
};
}();
// ---------------------------- ListingEditor.Core ----------------------------
/* ***********************************************************************
* ListingEditor.Core contains code that should be shared across different
* Wikivoyage languages. This code uses the custom configurations in the
* ListingEditor.Config and ListingEditor.Callback modules to initialize
* the listing editor and process add and update requests for listings.
* ***********************************************************************/
ListingEditor.Core = function() {
var api = new mw.Api();
var MODE_ADD = 'add';
var MODE_EDIT = 'edit';
// selector that identifies the edit link as created by the
// addEditButtons() function
var EDIT_LINK_SELECTOR = '.vcard-edit-button';
var SAVE_FORM_SELECTOR = '#progress-dialog';
var CAPTCHA_FORM_SELECTOR = '#captcha-dialog';
var sectionText, inlineListing, replacements = {};
/**
* Return false if the current page should not enable the listing editor.
* Examples where the listing editor should not be enabled include talk
* pages, edit pages, history pages, etc.
*/
var listingEditorAllowedForCurrentPage = function() {
var namespace = mw.config.get( 'wgNamespaceNumber' );
if (namespace !== 0 && namespace !== 2 && namespace !== 4) {
return false;
}
if ( mw.config.get('wgAction') != 'view' || $('#mw-revision-info').length
|| mw.config.get('wgCurRevisionId') != mw.config.get('wgRevisionId')
|| !mw.config.get('wgRelevantPageIsProbablyEditable')
|| $('#ca-viewsource').length ) {
return false;
}
return true;
};
/**
* Generate the form UI for the listing editor. If editing an existing
* listing, pre-populate the form input fields with the existing values.
*/
var createForm = function(mode, listingParameters, listingTemplateAsMap) {
var form = $(ListingEditor.Config.EDITOR_FORM_HTML);
// make sure the select dropdown includes any custom "type" values
listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER] =
listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER].toLowerCase().replace(" ", "_");
var listingType = listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER];
if (isCustomListingType(listingType)) {
$('#' + listingParameters[ListingEditor.Config.LISTING_TYPE_PARAMETER].id, form).append('<option value="' + listingType + '">' + listingType + '</option>');
}
// populate the empty form with existing values
for (var parameter in listingParameters) {
var parameterInfo = listingParameters[parameter];
if (listingTemplateAsMap[parameter]) {
$('#' + parameterInfo.id, form).val(listingTemplateAsMap[parameter]);
} else if (parameterInfo.hideDivIfEmpty) {
$('#' + parameterInfo.hideDivIfEmpty, form).hide();
}
}
for (var i=0; i < ListingEditor.Callbacks.CREATE_FORM_CALLBACKS.length; i++) {
ListingEditor.Callbacks.CREATE_FORM_CALLBACKS[i](form, mode);
}
return form;
};
/**
* Wrap the h2/h3 heading tag and everything up to the next section
* (including sub-sections) in a div to make it easier to traverse the DOM.
* This change introduces the potential for code incompatibility should the
* div cause any CSS or UI conflicts.
*/
var wrapContent = function() {
$('#bodyContent h2').each(function(){
$(this).nextUntil("h1, h2").addBack().wrapAll('<div class="mw-h2section" />');
});
$('#bodyContent h3').each(function(){
$(this).nextUntil("h1, h2, h3").addBack().wrapAll('<div class="mw-h3section" />');
});
};
/**
* Place an "add listing" link at the top of each section heading next to
* the "edit" link in the section heading.
*/
var addListingButtons = function() {
if ($(ListingEditor.Config.DISALLOW_ADD_LISTING_IF_PRESENT.join(',')).length > 0) {
return false;
}
for (var sectionId in ListingEditor.Config.SECTION_TO_TEMPLATE_TYPE) {
// 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');
$('h3 .mw-headline', parentHeading).each(function() {
insertAddListingPlaceholder(this);
});
}
}
$('.listingeditor-add').click(function() {
initListingEditorDialog(MODE_ADD, $(this));
})
.attr('title', ListingEditor.Config.TRANSLATIONS.addTitle);
};
/**
* Utility function for appending the "add listing" link text to a heading.
*/
var insertAddListingPlaceholder = function(parentHeading) {
var editSection = $(parentHeading).next('.mw-editsection');
if (editSection.length > 0)
editSection.append('<span class="mw-editsection-bracket">[</span><a href="javascript:" class="listingeditor-add">'+ListingEditor.Config.TRANSLATIONS.add+'</a><span class="mw-editsection-bracket">]</span>');
else {
// Mobile view: Minerva support
editSection = $(parentHeading).next('span');
editSection.after('<span><a href="javascript:" class="mw-ui-icon mw-ui-icon-element listingeditor-add">'+ListingEditor.Config.TRANSLATIONS.add+'</a></span>');
}
};
/**
* Place an "edit" link next to all existing listing tags.
*/
var addEditButtons = function() {
var editButton = $('<span class="vcard-edit-button noprint">')
.html('<a href="javascript:" class="listingeditor-edit">'+ListingEditor.Config.TRANSLATIONS.edit+'</a>' )
.click(function() {
initListingEditorDialog(MODE_EDIT, $(this));
})
.attr('title', ListingEditor.Config.TRANSLATIONS.editTitle);
// if there is already metadata present add a separator
$(ListingEditor.Config.EDIT_LINK_CONTAINER_SELECTOR).each(function() {
if (!isElementEmpty(this)) {
$(this).append('<span class="noprint"> | </span>');
} else {
$(this).append('<span class="noprint">(</span>');
$(this).parent().append('<span class="listing-metadata noprint">)</span>');
}
});
// append the edit link
$(ListingEditor.Config.EDIT_LINK_CONTAINER_SELECTOR).append( editButton );
};
/**
* Determine whether a listing entry is within a paragraph rather than
* an entry in a list; inline listings will be formatted slightly
* differently than entries in lists (no newlines in the template syntax,
* skip empty fields).
*/
var isInline = function(entry) {
// if the edit link clicked is within a paragraph AND, since
// newlines in a listing description will cause the Mediawiki parser
// to close an HTML list (thus triggering the "is edit link within a
// paragraph" test condition), also verify that the listing is
// within the expected listing template span tag and thus hasn't
// been incorrectly split due to newlines.
// return (entry.closest('p').length !== 0 && entry.closest('span.vcard').length !== 0);
return true;
};
/**
* 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;
// Mobile view: Minerva support
link = heading.find('.in-block a').attr('href');
return (link !== undefined) ? link.split('=').pop() : 0;
};
/**
* Given an edit link that was clicked for a listing, determine what index
* that listing is within a section. First listing is 0, second is 1, etc.
*/
var findListingIndex = function(sectionHeading, clicked) {
var count = 0;
$(EDIT_LINK_SELECTOR, sectionHeading).each(function() {
if (clicked.is($(this))) {
return false;
}
count++;
});
return count;
};
/**
* Return the listing template type appropriate for the section that
* contains the provided DOM element (example: "see" for "See" sections,
* etc). If no matching type is found then the default listing template
* type is returned.
*/
var findListingTypeForSection = function(entry) {
var sectionType = entry.closest('div.mw-h2section').children('h2').find('.mw-headline').attr('id');
for (var sectionId in ListingEditor.Config.SECTION_TO_TEMPLATE_TYPE) {
if (sectionType == sectionId) {
return ListingEditor.Config.SECTION_TO_TEMPLATE_TYPE[sectionId];
}
}
return ListingEditor.Config.DEFAULT_LISTING_TEMPLATE;
};
var replaceSpecial = function(str) {
return str.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&");
};
/**
* Return a regular expression that can be used to find all listing
* template invocations (as configured via the LISTING_TEMPLATES map)
* within a section of wikitext. Note that the returned regex simply
* matches the start of the template ("{{listing") and not the full
* template ("{{listing|key=value|...}}").
*/
var getListingTypesRegex = function() {
var regex = [];
for (var key in ListingEditor.Config.LISTING_TEMPLATES) {
regex.push(key);
}
return new RegExp('({{\\s*(' + regex.join('|') + ')\\b)(\\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 $.trim(listingSyntax);
};
var checkYesNo = function(value) {
var v = value.toLowerCase();
if (ListingEditor.Config.YES_ARRAY[v] === '') return 'y';
else if (ListingEditor.Config.NO_ARRAY[v] === '') return 'n';
else return '';
};
/**
* Convert raw wiki listing syntax into a mapping of key-value pairs
* corresponding to the listing template parameters.
*/
var wikiTextToListing = function(listingTemplateWikiSyntax) {
var typeRegex = getListingTypesRegex();
// convert "{{see" to {{listing|type=see"
// but not for vCard template
if ( !(ListingEditor.Config.DEFAULT_TEMPLATE_NO_TYPE &&
(listingTemplateWikiSyntax.toLowerCase().indexOf(ListingEditor.Config.DEFAULT_LISTING_TEMPLATE.toLowerCase()) >= 0) ) )
listingTemplateWikiSyntax = listingTemplateWikiSyntax.replace(typeRegex,'{{vCard| ' + ListingEditor.Config.LISTING_TYPE_PARAMETER + '=$2$3'); /* mod */
// remove the trailing braces
listingTemplateWikiSyntax = listingTemplateWikiSyntax.slice(0,-2);
var listingTemplateAsMap = {};
var lastKey;
var listParams = listingTemplateToParamsArray(listingTemplateWikiSyntax);
var key;
for (var j=1; j < listParams.length; j++) {
var param = listParams[j];
var index = param.indexOf('=');
if (index > 0) {
// param is of the form key=value
key = $.trim(param.substr(0, index));
var value = $.trim(param.substr(index+1));
listingTemplateAsMap[key] = value;
lastKey = key;
} else if (listingTemplateAsMap[lastKey].length) {
// there was a pipe character within a param value, such as
// "key=value1|value2", so just append to the previous param
listingTemplateAsMap[lastKey] += '|' + param;
}
}
for (key in listingTemplateAsMap) {
// if the template value contains an HTML comment that was
// previously converted to a placehold then it needs to be
// converted back to a comment so that the placeholder is not
// displayed in the edit form
listingTemplateAsMap[key] = restoreComments(listingTemplateAsMap[key], false);
}
if (listingTemplateAsMap[ListingEditor.Config.LISTING_CONTENT_PARAMETER]) {
// convert paragraph tags to newlines so that the content is more
// readable in the editor window
listingTemplateAsMap[ListingEditor.Config.LISTING_CONTENT_PARAMETER] = listingTemplateAsMap[ListingEditor.Config.LISTING_CONTENT_PARAMETER].replace(/\s*<p>\s*/g, '\n\n');
}
// sanitize the listing type param to match the configured values, so
// if the listing contained "Do" it will still match the configured "do"
if (!listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER])
listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER] = '';
for (key in ListingEditor.Config.LISTING_TEMPLATES) {
if (listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER].toLowerCase() === key.toLowerCase()) {
listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER] = key;
break;
}
}
for (key in listingTemplateAsMap) {
var c = checkYesNo( listingTemplateAsMap[key] );
if (c !== '') listingTemplateAsMap[key] = c;
}
return listingTemplateAsMap;
};
/**
* 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.
*/
var listingTemplateToParamsArray = function(listingTemplateWikiSyntax) {
var results = [];
var paramValue = '';
var pos = 0;
while (pos < listingTemplateWikiSyntax.length) {
var remainingString = listingTemplateWikiSyntax.substr(pos);
// check for a nested template or wikilink
var patternMatch = findPatternMatch(remainingString, "{{", "}}");
if (patternMatch.length === 0) {
patternMatch = findPatternMatch(remainingString, "[[", "]]");
}
if (patternMatch.length > 0) {
paramValue += patternMatch;
pos += patternMatch.length;
} else if (listingTemplateWikiSyntax.charAt(pos) === '|') {
// delimiter - push the previous param and move on to the next
results.push(paramValue);
paramValue = '';
pos++;
} else {
// append the character to the param value being built
paramValue += listingTemplateWikiSyntax.charAt(pos);
pos++;
}
}
if (paramValue.length > 0) {
// append the last param value
results.push(paramValue);
}
return results;
};
/**
* Utility method for finding a matching end pattern for a specified start
* pattern, including nesting. The specified value must start with the
* start value, otherwise an empty string will be returned.
*/
var findPatternMatch = function(value, startPattern, endPattern) {
var matchString = '';
var startRegex = new RegExp('^' + replaceSpecial(startPattern), 'i');
if (startRegex.test(value)) {
var endRegex = new RegExp('^' + replaceSpecial(endPattern), 'i');
var matchCount = 1;
for (var i = startPattern.length; i < value.length; i++) {
var remainingValue = value.substr(i);
if (startRegex.test(remainingValue)) {
matchCount++;
} else if (endRegex.test(remainingValue)) {
matchCount--;
}
if (matchCount === 0) {
matchString = value.substr(0, i);
break;
}
}
}
return matchString;
};
/**
* This method is invoked when an "add" or "edit" listing button is
* clicked and will execute an Ajax request to retrieve all of the raw wiki
* syntax contained within the specified section. This wiki text will
* later be modified via the listing editor and re-submitted as a section
* edit.
*/
var initListingEditorDialog = function(mode, clicked) {
var listingType;
if (mode === MODE_ADD) {
listingType = findListingTypeForSection(clicked);
}
var sectionHeading = findSectionHeading(clicked);
var sectionIndex = findSectionIndex(sectionHeading);
var listingIndex = (mode === MODE_ADD) ? -1 : findListingIndex(sectionHeading, clicked);
if (mode === MODE_EDIT) inlineListing = isInline(clicked);
else inlineListing = true;
$.ajax({
url: mw.util.wikiScript(''),
data: { title: mw.config.get('wgPageName'), action: 'raw', section: sectionIndex },
cache: false // required
}).done(function(data, textStatus, jqXHR) {
sectionText = data;
openListingEditorDialog(mode, sectionIndex, listingIndex, listingType);
}).fail(function(jqXHR, textStatus, errorThrown) {
alert(ListingEditor.Config.TRANSLATIONS.ajaxInitFailure + ': ' + textStatus + ' ' + errorThrown);
});
};
/**
* This method is called asynchronously after the initListingEditorDialog()
* method has retrieved the existing wiki section content that the
* listing is being added to (and that contains the listing wiki syntax
* when editing).
*/
var openListingEditorDialog = function(mode, sectionNumber, listingIndex, listingType) {
sectionText = stripComments(sectionText);
var listingTemplateAsMap, listingTemplateWikiSyntax;
if (mode == MODE_ADD) {
listingTemplateAsMap = {};
listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER] = listingType;
} else {
listingTemplateWikiSyntax = getListingWikitextBraces(listingIndex);
listingTemplateAsMap = wikiTextToListing(listingTemplateWikiSyntax);
listingType = listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER];
}
var listingParameters = getListingInfo();
// if a listing editor dialog is already open, get rid of it
wvDialog.destroy(); // destroy if existent
// $(ListingEditor.Config.EDITOR_FORM_SELECTOR).remove();
// $('.wv-dialog-background').remove();
var windowWidth = $(window).width();
var dialogWidth = (windowWidth > ListingEditor.Config.MAX_DIALOG_WIDTH) ? ListingEditor.Config.MAX_DIALOG_WIDTH : 'auto';
var leElement;
var leDialog = wvDialog.create({
width: dialogWidth,
title: (mode == MODE_ADD) ? ListingEditor.Config.TRANSLATIONS.addTitle : ListingEditor.Config.TRANSLATIONS.editTitle,
dialogClass: 'listing-editor-dialog',
id: 'listing-editor-dialog',
cancelTitle: ListingEditor.Config.TRANSLATIONS.cancelTitle
});
var leBody = wvDialog.getElement( 'body' );
leDialog.append( leBody );
var form = $(createForm(mode, listingParameters, listingTemplateAsMap));
leBody.append( form );
var leButtons = wvDialog.getElement( 'button-pane' );
leElement = wvDialog.getElement( 'button', 'listing-help' )
.text('?')
.attr('title', ListingEditor.Config.TRANSLATIONS.helpTitle)
.click(function() { window.open(ListingEditor.Config.TRANSLATIONS.helpPage); });
leButtons.append( leElement );
leElement = wvDialog.getElement( 'button' )
.text(ListingEditor.Config.TRANSLATIONS.submit)
.attr('title', ListingEditor.Config.TRANSLATIONS.submitTitle)
.click(function() {
if ($(ListingEditor.Config.EDITOR_CLOSED_SELECTOR).is(':checked')) {
// no validation
formToText(mode, listingTemplateWikiSyntax, listingTemplateAsMap, sectionNumber);
wvDialog.destroy();
}
else if (validateForm()) {
formToText(mode, listingTemplateWikiSyntax, listingTemplateAsMap, sectionNumber);
wvDialog.destroy();
}
});
leButtons.append( leElement );
leElement = wvDialog.getElement( 'button', 'listing-preview-button' )
.text(ListingEditor.Config.TRANSLATIONS.preview)
.attr('title', ListingEditor.Config.TRANSLATIONS.previewTitle)
.click(function() { formToPreview(listingTemplateAsMap); });
leButtons.append( leElement );
leElement = wvDialog.getElement( 'button', 'listing-previewOff' )
.text(ListingEditor.Config.TRANSLATIONS.previewOff)
.attr('title', ListingEditor.Config.TRANSLATIONS.previewOffTitle)
.attr('style', 'display: none')
.click(function() { hidePreview(); });
leButtons.append( leElement );
leElement = wvDialog.getElement( 'button', 'listing-refresh' )
.text(ListingEditor.Config.TRANSLATIONS.refresh)
.attr('title', ListingEditor.Config.TRANSLATIONS.refreshTitle)
.attr('style', 'display: none')
.click(function() { refreshPreview(listingTemplateAsMap); });
leButtons.append( leElement );
leElement = wvDialog.getElement( 'button' )
.text(ListingEditor.Config.TRANSLATIONS.cancel)
.attr('title', ListingEditor.Config.TRANSLATIONS.cancelTitle)
.click(function() { wvDialog.destroy() });
leButtons.append( leElement );
leElement = wvDialog.getElement( 'footer' ).append(leButtons);
leElement.append( wvDialog.getElement( 'listing-license' )
.html( ListingEditor.Config.TRANSLATIONS.licenseText) );
leDialog.append( leElement );
wvDialog.centerDialog( leDialog );
var testdata = [ 'abc', 'def', 'ghi', 'jkl', 'mno',
'ActionScript',
'AppleScript',
'Asp',
'BASIC',
'C',
'C++',
'Clojure',
'COBOL',
'ColdFusion',
'Erlang',
'Fortran',
'Groovy',
'Haskell',
'Java',
'JavaScript',
'Lisp',
'Perl',
'PHP',
'Python',
'Ruby',
{ value: 'Scala', label: 'ScalaTest' },
'Scheme'];
wvAutocomplete.init({
selector: $('#input-wikidata-label'),
source: testdata
});
wvAutocomplete.init({
selector: $('#input-image'),
source: ['abc', 'aab', 'aac', 'def', 'ghi', 'jkl', 'mno']
// function ( request, response )
// { response = ['abc', 'aab', 'aac', 'def', 'ghi', 'jkl', 'mno'] }
});
};
/**
* Commented-out listings can result in the wrong listing being edited, so
* strip out any comments and replace them with placeholders that can be
* restored prior to saving changes.
*/
var stripComments = function(text) {
var comments = text.match(/<!--[\s\S]*?-->/mig);
if (comments !== null ) {
for (var i = 0; i < comments.length; i++) {
var comment = comments[i];
var rep = '<<<COMMENT' + i + '>>>';
text = text.replace(comment, rep);
replacements[rep] = comment;
}
}
return text;
};
/**
* Search the text provided, and if it contains any text that was
* previously stripped out for replacement purposes, restore it.
*/
var restoreComments = function(text, resetReplacements) {
for (var key in replacements) {
var val = replacements[key];
text = text.replace(key, val);
}
if (resetReplacements) {
replacements = {};
}
return text;
};
/**
* Given a listing type, return the appropriate entry from the
* LISTING_TEMPLATES array. This method returns the entry for the default
* listing template type if not enty exists for the specified type.
*/
var getListingInfo = function() { // vCard specific
return ListingEditor.Config.LISTING_TEMPLATE_PARAMETERS;
};
/**
* Determine if the specified listing type is a custom type - for example "go"
* instead of "see", "do", "listing", etc.
*/
var isCustomListingType = function(listingType) { // vCard specific
for (var i = 0; i < ListingEditor.Config.LISTING_TYPE_OPTIONS.length; i++) {
if (ListingEditor.Config.LISTING_TYPE_OPTIONS[i].type === listingType) return false;
}
return true;
};
/**
* Logic invoked on form submit to analyze the values entered into the
* editor form and to block submission if any fatal errors are found.
*/
var validateForm = function() {
var validationFailureMessages = [];
for (var i=0; i < ListingEditor.Callbacks.VALIDATE_FORM_CALLBACKS.length; i++) {
ListingEditor.Callbacks.VALIDATE_FORM_CALLBACKS[i](validationFailureMessages);
}
if (validationFailureMessages.length > 0) {
alert(validationFailureMessages.join('\n'));
return false;
}
// newlines in listing content won't render properly in lists, so
// replace them with <p> tags
$('#input-content').val($.trim($('#input-content').val()).replace(/\n+/g, '<p>'));
var webRegex = new RegExp('^https?://', 'i');
var url = $('#input-url').val();
if (!webRegex.test(url) && url !== '') {
$('#input-url').val('http://' + url);
}
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 formToText = function(mode, listingTemplateWikiSyntax, listingTemplateAsMap, sectionNumber) {
var listing = listingTemplateAsMap;
var listingParameters = getListingInfo();
var listingTypeInput = listingParameters[ListingEditor.Config.LISTING_TYPE_PARAMETER].id;
var listingType = $("#" + listingTypeInput).val();
for (var parameter in listingParameters) {
listing[parameter] = $("#" + listingParameters[parameter].id).val();
}
for (var i=0; i < ListingEditor.Callbacks.SUBMIT_FORM_CALLBACKS.length; i++) {
ListingEditor.Callbacks.SUBMIT_FORM_CALLBACKS[i](listing, mode);
}
var text = listingToStr(listing);
var summary = editSummarySection();
if (mode == MODE_ADD) {
summary = updateSectionTextWithAddedListing(summary, text, listing);
} else {
summary = updateSectionTextWithEditedListing(summary, text, listingTemplateWikiSyntax);
}
summary += $("#input-name").val();
if ($(ListingEditor.Config.EDITOR_SUMMARY_SELECTOR).val() !== '') {
summary += ' – ' + $(ListingEditor.Config.EDITOR_SUMMARY_SELECTOR).val();
}
if ( mw.config.get('skin') === 'minerva' ) {
summary += ' – ' + ListingEditor.Config.TRANSLATIONS.mobileEdit;
}
var minor = $(ListingEditor.Config.EDITOR_MINOR_EDIT_SELECTOR).is(':checked') ? true : false;
saveForm(summary, minor, sectionNumber, '', '');
return;
};
var showPreview = function(listingTemplateAsMap) {
var listing = listingTemplateAsMap;
var listingParameters = getListingInfo();
var listingTypeInput = listingParameters[ListingEditor.Config.LISTING_TYPE_PARAMETER].id;
var listingType = $("#" + listingTypeInput).val();
for (var parameter in listingParameters) {
listing[parameter] = $("#" + listingParameters[parameter].id).val();
}
var text = listingToStr(listing);
$.ajax ({
url: mw.config.get('wgScriptPath') + '/api.php?' + $.param({
action: 'parse',
prop: 'text',
contentmodel: 'wikitext',
format: 'json',
'text': text,
}),
error: function (jqXHR, txt) {
$('#listing-preview').hide();
},
success: function (data) {
$('#listing-preview-text').html(data.parse.text['*']);
},
});
};
var formToPreview = function(listingTemplateAsMap) {
if ( !$('#listing-preview').is(':visible') ) {
$('#listing-preview').show();
$('#listing-refresh').show();
$('#listing-preview-button').hide();
$('#listing-previewOff').show();
showPreview(listingTemplateAsMap);
} else {
$('#listing-preview').hide();
$('#listing-refresh').hide();
$('#listing-previewOff').hide();
$('#listing-preview-button').show();
}
};
var refreshPreview = function(listingTemplateAsMap) {
if ( $('#listing-preview').is(':visible') ) {
showPreview(listingTemplateAsMap);
}
};
var hidePreview = function() {
$('#listing-preview').hide();
$('#listing-previewOff').hide();
$('#listing-refresh').hide();
$('#listing-preview-button').show();
};
/**
* 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) {
var summary = originalEditSummary;
summary += ListingEditor.Config.TRANSLATIONS.added;
// 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, listingTemplateWikiSyntax) {
var summary = originalEditSummary;
if ($(ListingEditor.Config.EDITOR_CLOSED_SELECTOR).is(':checked')) {
listingWikiText = '';
summary += ListingEditor.Config.TRANSLATIONS.removed;
var listRegex = new RegExp('(\\n+[\\:\\*\\#]*)?\\s*' + replaceSpecial(listingTemplateWikiSyntax));
sectionText = sectionText.replace(listRegex, listingWikiText);
} else {
summary += ListingEditor.Config.TRANSLATIONS.updated;
sectionText = sectionText.replace(listingTemplateWikiSyntax, listingWikiText);
}
sectionText = restoreComments(sectionText, true);
return summary;
};
/**
* Render a dialog that notifies the user that the listing editor changes
* are being saved.
*/
var savingForm = function() {
// if a progress dialog is already open, get rid of it
// Problem: Minerva
/* if ($(SAVE_FORM_SELECTOR).length > 0) {
$(SAVE_FORM_SELECTOR).dialog('destroy').remove();
}
var progress = $('<div id="progress-dialog">' + ListingEditor.Config.TRANSLATIONS.saving + '</div>');
progress.dialog({
modal: true,
height: 100,
width: 300,
title: ''
});
$(".ui-dialog-titlebar").hide(); */
};
/**
* Execute the logic to post listing editor changes to the server so that
* they are saved. After saving the page is refreshed to show the updated
* article.
*/
var saveForm = function(summary, minor, sectionNumber, cid, answer) {
var editPayload = {
action: "edit",
title: mw.config.get( "wgPageName" ),
section: sectionNumber,
text: sectionText,
summary: summary,
captchaid: cid,
captchaword: answer
};
if (minor) {
$.extend( editPayload, { minor: 'true' } );
}
api.postWithToken(
"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.escapeId(getSectionName());
if (sectionName.length) {
canonicalUrl += "#" + sectionName;
}
window.location.href = canonicalUrl;
} else {
window.location.reload();
}
} else if (data && data.error) {
saveFailed(ListingEditor.Config.TRANSLATIONS.submitApiError + ' "' + data.error.code + '": ' + data.error.info );
} else if (data && data.edit.spamblacklist) {
saveFailed(ListingEditor.Config.TRANSLATIONS.submitBlacklistError + ': ' + data.edit.spamblacklist );
} else if (data && data.edit.captcha) {
// Problem: Minerva
/* $(SAVE_FORM_SELECTOR).dialog('destroy').remove();
captchaDialog(summary, minor, sectionNumber, data.edit.captcha.url, data.edit.captcha.id); */
} else {
saveFailed(ListingEditor.Config.TRANSLATIONS.submitUnknownError);
}
}).fail(function(code, result) {
if (code === "http") {
saveFailed(ListingEditor.Config.TRANSLATIONS.submitHttpError + ': ' + result.textStatus );
} else if (code === "ok-but-empty") {
saveFailed(ListingEditor.Config.TRANSLATIONS.submitEmptyError);
} else {
saveFailed(ListingEditor.Config.TRANSLATIONS.submitUnknownError + ': ' + code );
}
});
savingForm();
};
/**
* If an error occurs while saving the form, remove the "saving" dialog,
* restore the original listing editor form (with all user content), and
* display an alert with a failure message.
*/
var saveFailed = function(msg) {
// Problem: Minerva
// $(SAVE_FORM_SELECTOR).dialog('destroy').remove();
// $(ListingEditor.Config.EDITOR_FORM_SELECTOR).dialog('open');
alert(msg);
};
/**
* If the result of an attempt to save the listing editor content is a
* Captcha challenge then display a form to allow the user to respond to
* the challenge and resubmit.
*/
var captchaDialog = function(summary, minor, sectionNumber, captchaImgSrc, captchaId) {
// if a captcha dialog is already open, get rid of it
// Problem: Minerva
/* if ($(CAPTCHA_FORM_SELECTOR).length > 0) {
$(CAPTCHA_FORM_SELECTOR).dialog('destroy').remove();
}
var captcha = $('<div id="captcha-dialog">').text(ListingEditor.Config.TRANSLATIONS.externalLinks);
var image = $('<img class="fancycaptcha-image">')
.attr('src', captchaImgSrc)
.appendTo(captcha);
var label = $('<label for="input-captcha">').text(ListingEditor.Config.TRANSLATIONS.enterCaptcha).appendTo(captcha);
var input = $('<input id="input-captcha" type="text">').appendTo(captcha);
captcha.dialog({
modal: true,
title: ListingEditor.Config.TRANSLATIONS.enterCaptcha,
buttons: [
{
text: ListingEditor.Config.TRANSLATIONS.submit, click: function() {
saveForm(summary, minor, sectionNumber, captchaId, $('#input-captcha').val());
$(this).dialog('destroy').remove();
}
},
{
text: ListingEditor.Config.TRANSLATIONS.cancel, click: function() {
$(this).dialog('destroy').remove();
}
}
]
}); */
};
/**
* Convert the listing map back to a wiki text string.
*/
var listingToStr = function(listing) {
var listingType = listing[ListingEditor.Config.LISTING_TYPE_PARAMETER];
var listingParameters = getListingInfo();
var saveStr = '{{';
saveStr += ListingEditor.Config.DEFAULT_LISTING_TEMPLATE;
saveStr += ' | ' + ListingEditor.Config.LISTING_TYPE_PARAMETER + ' = ' + listingType.replace("_", " ") + ' ';
if (!inlineListing && listingParameters[ListingEditor.Config.LISTING_TYPE_PARAMETER].newline) {
saveStr += '\n';
}
var skipIt;
for (var parameter in listingParameters) {
if (parameter === ListingEditor.Config.LISTING_TYPE_PARAMETER) {
// "type" parameter was handled previously
continue;
}
if (parameter === ListingEditor.Config.LISTING_CONTENT_PARAMETER) {
// processed last
continue;
}
skipIt = listingParameters[parameter].skipIfEmpty;
if (typeof(skipIt) === "undefined") skipIt = ListingEditor.Config.SKIP_DEFAULT;
if (listing[parameter] !== '' || (!skipIt && !inlineListing)) {
saveStr += '| ' + parameter + ' = ' + listing[parameter];
}
if (!saveStr.match(/\n$/)) {
if (!inlineListing && listingParameters[parameter].newline) {
saveStr = rtrim(saveStr) + '\n';
} else if (!saveStr.match(/ $/)) {
saveStr += ' ';
}
}
}
if (ListingEditor.Config.ALLOW_UNRECOGNIZED_PARAMETERS) {
// append any unexpected values
for (var key in listing) {
if (listingParameters[key]) {
// this is a known field
continue;
}
if (listing[key] === '') {
// skip unrecognized fields without values
continue;
}
saveStr += '| ' + key + ' = ' + listing[key];
saveStr += (inlineListing) ? ' ' : '\n';
}
}
saveStr += '| ' + ListingEditor.Config.LISTING_CONTENT_PARAMETER + ' = ' + listing[ListingEditor.Config.LISTING_CONTENT_PARAMETER];
saveStr += (inlineListing || !listingParameters[ListingEditor.Config.LISTING_CONTENT_PARAMETER].newline) ? ' ' : '\n';
saveStr += '}}';
return saveStr;
};
/**
* Determine if the specified DOM element contains only whitespace or
* whitespace HTML characters ( ).
*/
var isElementEmpty = function(element) {
var text = $(element).text();
if (!text.trim()) {
return true;
}
return (text.trim() == ' ');
};
/**
* Trim whitespace at the end of a string.
*/
var rtrim = function(str) {
return str.replace(/\s+$/, '');
};
/**
* Trim decimal precision if it exceeds the specified number of
* decimal places.
*/
var trimDecimal = function(value, precision) {
if ($.isNumeric(value) && value.toString().length > value.toFixed(precision).toString().length) {
value = value.toFixed(precision);
}
return value;
};
/**
* Called on DOM ready, this method initializes the listing editor and
* adds the "add/edit listing" links to sections and existing listings.
*/
var initListingEditor = function() {
if (!listingEditorAllowedForCurrentPage()) {
return;
}
wrapContent();
mw.hook( 'wikipage.content' ).add( addListingButtons.bind( this ) );
addEditButtons();
};
// expose public members
return {
MODE_ADD: MODE_ADD,
MODE_EDIT: MODE_EDIT,
trimDecimal: trimDecimal,
init: initListingEditor
};
}();
// -------------------------- jquery.ui dialog replacement ---------------------------
/* ***********************************************************************
* jquery.ui replacement.
* ***********************************************************************/
var wvDialog = function() {
var create = function( options ) {
var classes = 'wv-dialog';
if (options.dialogClass) classes += ' ' + options.dialogClass;
classes = ' class="' + classes + '"';
var id = '';
if (options.id) id = ' id="' + options.id + '"';
var width = 'auto';
if (options.width) width = options.width;
var height = 'auto';
if (options.height) height = options.height;
var draggable = true;
if (typeof(options.draggable) === 'boolean') draggable = options.draggable;
var resizable = true;
if (typeof(options.resizable) === 'boolean') resizable = options.resizable;
var closeOnEscape = true;
if (typeof(options.closeOnEscape) === 'boolean') closeOnEscape = options.closeOnEscape;
$('body').append( $('<div class="wv-dialog-background"></div>') );
var aDialog = $('<div' + classes + id + '></div>')
.css('width', width)
.css('height', height);
$('body').append( aDialog );
if (options.title)
appendHeader( aDialog, options.title, options.cancelTitle, draggable );
if (closeOnEscape)
$(document).keyup(function(e) { if ( e.keyCode === 27 ) destroy() } );
return aDialog;
};
var dialog;
var mousePos = {};
var dialogPos = {};
var dialogMove = function(e) {
dialog.css('left', dialogPos.X - mousePos.X + e.clientX)
.css('top', dialogPos.Y - mousePos.Y + e.clientY);
};
var appendHeader = function( aDialog, aTitle, cancelTitle, draggable ) {
if ( !aDialog || !aTitle || (aTitle === '') ) return;
var cTitle = 'Abort dialog';
if ( cancelTitle && (cancelTitle !== '') ) cTitle = cancelTitle;
var anElement = $( '<div class="wv-dialog-close"></div>' )
.attr('title', cTitle)
.click(function() { wvDialog.destroy() });
aDialog.append( $( anElement ) );
anElement = $( '<div class="wv-dialog-header"></div>' )
.text( aTitle );
if (draggable) anElement
.css('cursor', 'move')
.mouseup(function(e) {
$(this).off( 'mousemove', dialogMove ).css('cursor', 'move');
})
.mouseout(function(e) {
$(this).off( 'mousemove', dialogMove ).css('cursor', 'move');
})
.mousedown(function(e) {
dialog = $(this).parent();
$(this).on( 'mousemove', dialogMove ).css('cursor', 'grabbing');
mousePos.X = e.clientX;
mousePos.Y = e.clientY;
dialogPos.X = parseInt( dialog.css('left') );
dialogPos.Y = parseInt( dialog.css('top') );
});
aDialog.append( anElement );
};
var centerDialog = function( aDialog ) {
if ( !aDialog) return;
aDialog.css('left', (document.body.scrollWidth - aDialog.width()) / 2 + $(document).scrollLeft() )
.css('top', (window.innerHeight - aDialog.height()) / 2 + $(document).scrollTop() );
};
var getElement = function( aType, anId ) {
if (!aType) return;
var tag = 'div';
var classes = '';
var id = '';
if (anId) id = ' id="' + anId + '"';
switch( aType ) {
case 'body':
classes = 'wv-dialog-body';
break;
case 'footer':
classes = 'wv-dialog-footer';
break;
case 'button-pane':
classes = 'wv-dialog-button-pane';
break;
case 'button':
classes = 'wv-dialog-button';
tag = 'button';
break;
default:
classes = aType;
}
classes = ' class="' + classes + '"';
return $( '<' + tag + classes + id +'></' + tag + '>' );
};
var destroy = function() {
if ( $('.wv-dialog-background').length > 0 ) $('.wv-dialog-background').remove();
if ( $('.wv-dialog').length > 0 ) $('.wv-dialog').remove();
};
return {
appendHeader: appendHeader,
centerDialog: centerDialog,
create: create,
destroy: destroy,
getElement: getElement
};
}();
// -------------------- jquery.ui autocomplete replacement --------------------
/* ***********************************************************************
* jquery.ui replacement.
* ***********************************************************************/
var wvAutocomplete = function() {
var opts = {};
var init = function( options ) {
var selector = options.selector;
selector.keyup(function(e) { keyevent( selector, e ); })
.focusout(function(e) { closeList( selector ) });
var id = selector.attr('id');
if (!opts[id]) {
opts[id] = {
data: null,
classes: '',
source: null,
select: null,
_renderItem: function( list, item ) {
return $( '<li></li>')
.append( item.label )
.appendTo( list );
},
matches: [],
focus: -1,
lastValue: ''
};
}
if ( $.isArray( options.source ) ) {
opts[id].data = options.source;
} else if ( typeof options.source === 'string' ) {
// not supported
} else {
opts[id].source = options.source;
}
if (options.select) opts[id].select = options.select;
if (options.classes) opts[id].classes = ' ' + options.classes;
};
var keyevent = function( thisInput, e ) {
var input = thisInput.val();
if (input === '') {
closeList( thisInput );
return;
}
var list = $('#wvac-' + thisInput.attr('id'));
var listExists = list.length > 0;
var isEsc = false;
var id = thisInput.attr('id');
switch(e.keyCode) {
case 13: // enter
closeList( thisInput );
break;
case 27: // escape
if (listExists) {
closeList( thisInput );
e.preventDefault();
e.stopPropagation();
}
isEsc = true;
break;
case 38: // up
updateFocus( thisInput, -1 );
break;
case 40: // down
updateFocus( thisInput, 1 );
break;
default:
if ( opts[id].lastValue !== input) {
closeList( thisInput );
listExists = false;
}
}
if ( !listExists && !isEsc) search( thisInput );
};
var setValue = function( thisInput, v ) {
thisInput.val( v );
var id = thisInput.attr('id');
var ui = { item: {} } ;
if ( (opts[id].select !== null) && (typeof(opts[id].select) === "function") ) {
var event = null;
ui.item.value = v;
opts[id].select( event, ui );
}
};
var setFocus = function( thisInput, f ) {
var id = thisInput.attr('id');
if ( (f < -1) || (f > opts[id].matches.length) ) return;
opts[id].focus = f;
var list = $('#wvac-' + id).children();
var i = 0;
var newValue = opts[id].lastValue;
list.each(function() {
if (i == f) {
$(this).addClass('wv-ac-focused');
newValue = opts[id].matches[i].value;
}
else $(this).removeClass('wv-ac-focused');
i++;
});
setValue( thisInput, newValue );
};
var updateFocus = function( thisInput, diff ) {
var id = thisInput.attr('id');
setFocus( thisInput, opts[id].focus + diff );
};
var setMouseFocus = function( thisInput, e ) {
var list = $('#wvac-' + thisInput.attr('id')).children();
var i = 0;
list.each(function() {
if ( $(this).is(e.target) ) setFocus( thisInput, i );
i++;
});
};
var defaultFilter = function( testStr, array ) {
var pattern = new RegExp(testStr, 'i');
return $.grep( array, function( v ) {
return pattern.test( v.label || v.value || v );
});
};
var search = function( thisInput ) {
var id = thisInput.attr('id');
opts[id].lastValue = thisInput.val();
opts[id].matches = [];
if ( opts[id].data !== null ) {
for (var i = 0; i < opts[id].data.length; i++) {
if ( typeof(opts[id].data[i]) !== 'object' )
opts[id].data[i] = { value: opts[id].data[i], label: opts[id].data[i] };
}
opts[id].matches = defaultFilter( opts[id].lastValue, opts[id].data );
}
openList( thisInput, opts[id].matches );
};
var openList = function( thisInput, matches ) {
var id = thisInput.attr('id');
var listId = 'wvac-' + id;
var list = $('#' + listId);
if ( list.length > 0 ) list.remove();
if ( matches.length > 0 ) {
list = $( '<ul class="wv-ac-list' + opts[id].classes + '" id="' + listId + '"></ul>' )
.css('left', thisInput.offset().left)
.css('top', thisInput.offset().top + thisInput.outerHeight());
$('body').append( list );
opts[id].focus = -1;
var item;
for (var i = 0; i < matches.length; i++) {
item = opts[id]._renderItem( list, matches[i] )
.attr( 'data-value', matches[i].value )
.mousedown(function(e) { setMouseFocus( thisInput, e ) } )
.mouseover(function(e) { setFocus( thisInput, -1 ) } );
}
}
};
var closeList = function( thisInput ){
var id = thisInput.attr('id');
var list = $('#wvac-' + id);
if ( list.length !== 0 ) list.remove();
opts[id].lastValue = '';
};
return {
init: init,
};
}();
// ------------------------------ Initialization ------------------------------
/* ***********************************************************************
* Editor initialization
* ***********************************************************************/
var suppressLE = false;
if (typeof window.suppressListingEditorBeta !== 'undefined') {
suppressLE = window.suppressListingEditorBeta;
}
if (!suppressLE) {
mw.loader.using(['mediawiki.util', 'jquery.ui' ]);
// mw.loader.using(['mediawiki.util', 'jquery.ui' ]).then( function () {
$(document).ready(function() {
ListingEditor.Core.init();
});
// });
}
} ( mediaWiki, jQuery ) );
//</nowiki>