משתמש:Orwell1/ListingEditor2.2.js
מראה
הערה: לאחר הפרסום, ייתכן שיהיה צורך לנקות את זיכרון המטמון (cache) של הדפדפן כדי להבחין בשינויים.
- פיירפוקס / ספארי: להחזיק את המקש Shift בעת לחיצה על טעינה מחדש (Reload) או ללחוץ על צירוף המקשים Ctrl-F5 או Ctrl-R (במחשב מק: ⌘-R).
- גוגל כרום: ללחוץ על צירוף המקשים Ctrl-Shift-R (במחשב מק: ⌘-Shift-R).
- אדג': להחזיק את המקש Ctrl בעת לחיצה על רענן (Refresh) או ללחוץ על צירוף המקשים Ctrl-F5.
/******************************************************************
Listing Editor v2.2
Original author:
- torty3
Additional contributors:
- Andyrom75
- ARR8
- RolandUnger
- Wrh2
v2.2 Changes:
- Create bidirectional Wikidata Sync window with reference support
- Automatically fill IATA code for airports iff Alt is empty
- Sync directions with Wikidata
- Trim period from price and address
- Import preview functionality from German Wikivoyage
- Add button to get Wikidata ID from given Wikipedia article
- Fx linterrors by phasing out <p> in favor of <br>
- Minor cleanup of template format
- Display color in type selectors
- Detect RTL text in alt field
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.
TODO
- Do not perform listing validations when deleting a listing.
- Add support for mobile devices.
- Add support for non-standard listing "type" values ("go", etc).
- wrapContent is breaking the expand/collapse logic on the VFD page.
- populate the input-type select list from LISTING_TEMPLATES
- Allow syncing Wikipedia link back to Wikidata with wbsetsitelink
- Allow hierarchy of preferred sources, rather than just one source, for Wikidata sync
- E.g. get URL with language of work = english before any other language version if exists
- E.g. get fall back to getting coordinate of headquarters if geographic coordinates unavailable. Prioritize getting coordinates of entrance before any other coordinates if all present
- E.g. Can use multiple sources to fetch address
- Figure out how to get this to upload properly
- Expand handling of references. Some automatic addition of 'imported from' this language's Wikivoyage
********************************************************************/
//<nowiki>
mw.loader.using(['mediawiki.util', 'jquery.ui'], ( function ( mw, $ ) {
'use strict';
importStylesheet('MediaWiki:NewListingEditor.css');
importScript("MediaWiki:ClockPicker.js");
/* ***********************************************************************
* CUSTOMIZATION INSTRUCTIONS:
*
* Different Wikivoyage language versions have different implementations of
* the listing template, so this module must be customized for each. The
* ListingEditor.Config and ListingEditor.Callbacks modules should be the
* ONLY code that requires customization - ListingEditor.Core should be
* shared across all language versions. If for some reason the Core module
* must be modified, ideally the module should be modified for all language
* versions so that the code can stay in sync.
* ***********************************************************************/
var ListingEditor = {};
// see http://toddmotto.com/mastering-the-module-pattern/ for an overview
// of the module design pattern being used in this gadget
/* ***********************************************************************
* ListingEditor.Config contains properties that will likely need to be
* modified for each Wikivoyage language version. Properties in this
* module will be referenced from the other ListingEditor modules.
* ***********************************************************************/
ListingEditor.Config = function() {
// --------------------------------------------------------------------
// TRANSLATE THE FOLLOWING BASED ON THE WIKIVOYAGE LANGUAGE IN USE
// --------------------------------------------------------------------
var LANG = 'he';
var WIKIDATAID = '24238020'; // wikidata ID of this wikivoyage edition
var COMMONS_URL = '//commons.wikimedia.org';
var WIKIDATA_URL = '//www.wikidata.org';
var WIKIPEDIA_URL = '//he.wikipedia.org';
var WIKIDATA_SITELINK_WIKIPEDIA = 'hewiki';
var TRANSLATIONS = {
'addTitle' : 'הוספת רשומה חדשה',
'editTitle' : 'עריכת רשומה קיימת',
'syncTitle' : 'Wikidata Sync',
'add': 'הוספת רשומה',
'edit': 'עריכה',
'saving': 'בשמירה...',
'submit': 'פרסום',
'cancel': 'ביטול',
'preview': 'תצוגה מקדימה',
'previewOff': 'סגירת תצוגה מקדימה',
'refresh': '↺', // \ue031 not yet working
'refreshTitle': 'רענון התצוגה המקדימה',
'selectAll': 'Select all',
'addExpandedTitle': 'הוספת רשומה חדשה באמצעות עורך הרשימות',
'validationEmptyListing': 'אנא הזינו שם כלשהו או כתובת',
'validationEmail': 'אנא ודאו שכתובת המייל שהוזנה תקינה',
'validationWikipedia': 'אנא הכניסו את שם הערך המקביל בוויקיפדיה כמו שהוא, אין להכניס כתובת URL',
'validationImage': 'אנא הכניסו את הכותרת של התמונה בוויקישיתוף ללא קידומת',
'image': '', //Local prefix for Image (or File)
'added': 'הוספת הרשומה: ',
'updated': 'עדכון הרשומה: ',
'removed': 'הסרת הרשומה: ',
'helpPage': 'http://he.wikivoyage.org/wiki/ויקימסע:עורך הרשומות',
'enterCaptcha': 'הזינו CAPTCHA',
'externalLinks': 'עריכתכם כוללת הוספת קישורים חיצוניים חדשים.',
// license text should match MediaWiki:Wikimedia-copyrightwarning
'licenseText': 'לחיצה על "פרסום" מהווה הסכמתך ל<a class="external" target="_blank" href="//wikimediafoundation.org/wiki/Terms_of_use">תנאי השימוש</a> ואת הסכמתך הבלתי־חוזרת לשחרר את תרומתך בכפוף לרישיון <a class="external" target="_blank" href="//en.wikivoyage.org/wiki/Wikivoyage:Full_text_of_the_Attribution-ShareAlike_3.0_license">CC-BY-SA 3.0 License</a> ולרישיון GFDL. זוהי גם הסכמתך לכך שקישור או כתובת URL הוא ייחוס מספיק בהתאם לרישיון Creative Commons.',
'ajaxInitFailure': 'שגיאה: אתחול עורך הרשומות נכשל',
'sharedImage': 'תמונה',
'sharedFacebook': 'פייסבוק',
'sharedLatitude': 'רוחב (lat)',
'sharedLongitude': 'אורך (long)',
'sharedWebsite': 'אתר אינטרנט',
'sharedWikipedia': 'ויקיפדיה',
'sharedUnesco': 'אתר מורשת עולמית: כן',
'submitApiError': 'שגיאה: The server returned an error while attempting to save the listing, please try again',
'submitBlacklistError': 'Error: A value in the data submitted has been blacklisted, please remove the blacklisted pattern and try again',
'submitUnknownError': 'Error: An unknown error has been encountered while attempting to save the listing, please try again',
'submitHttpError': 'Error: The server responded with an HTTP error while attempting to save the listing, please try again',
'submitEmptyError': 'Error: The server returned an empty response while attempting to save the listing, please try again',
'viewCommonsPage' : 'צפו בעמוד התמונה בוויקישיתוף',
'viewWikidataPage' : 'צפו בפריט המקביל בוויקינתונים',
'viewWikipediaPage' : 'צפו בערך המקביל בוויקיפדיה',
'wikidataShared': 'הנתונים הבאים התקבלו מהפריט בוויקינתונים המקביל לרשומה זו. האם לעדכן את שדות המידע על סמך הנתונים החדשים?',
'wikidataSharedNotFound': 'לא נמצאו נתונים חדשים בפריט המקביל בוויקינתונים',
'wikidataSyncBlurb': 'Selecting a value will change both websites to match (selecting an empty value will delete from both). Selecting neither will change nothing. Please err toward selecting one of the values rather than skipping - there are few cases when we should prefer to have a different value intentionally.<p>You are encouraged to go to the Wikidata item and add references for any data you change.',
'wikidataRemoteCoords': 'Coordinates are synchronized.'
};
// --------------------------------------------------------------------
// TRANSLATE AND CONFIGURE
// --------------------------------------------------------------------
var WIKIDATA_CLAIMS = {
//'lat': { 'p': 'P625', 'label': 'latitude', 'fields': 'lat', },
//'long': { 'p': 'P625', 'label': 'longitude', 'fields': 'long', },
'coords': { 'p': 'P625', 'label': 'קואורדינטות', 'fields': ['lat', 'long']},
'url': { 'p': 'P856', 'label': 'האתר הרשמי', 'fields': ['האתר הרשמי'], }, // link
'facebook': { 'p': 'P2013', 'label': 'פייסבוק', 'fields': ['פייסבוק'], },
'image': { 'p': 'P18', 'label': 'תמונה', 'fields': ['תמונה'], },
'iata': { 'p': 'P238', 'label': 'קוד יאט"א', 'fields': ['שם חלופי'], 'doNotUpload': true, },
'directions': { 'p': 'P2795', 'label': 'הוראות', 'fields': ['הוראות'], },
'email': { 'p': 'P968', 'label': 'מייל', 'fields': ['מייל'], },
};
// --------------------------------------------------------------------
// 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;
// set this flag to true if the listing editor should copy geographic
// coordinates from Wikidata, rather than the default behavior of leaving
// the lat and long fields empty (to auto-acquire the coordinates)
var LOCALLY_SYNC_COORDS = 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 = {
'מוקדי': 'מוקדי',
'מוקדי_עניין': 'מוקדי',
'מוקדי_העניין': 'מוקדי',
'מוקדי_העניין_העיקריים': 'מוקדי',
'אטרקציות': 'מוקדי',
'פעילויות': 'פעילויות',
'מסלולים': 'פעילויות',
'מסלולי_הליכה': 'פעילויות',
'אירועים_נוספים': 'פעילויות',
'בידור': 'בידור',
'בתי קולנוע': 'בידור',
'תיאטראות': 'בידור',
'בתי אופרה': 'בידור',
'קולנוע': 'בידור',
'קניות': 'קניות',
'קניונים': 'קניות',
'שווקים': 'קניות',
'אוכל': 'אוכל',
'שתייה': 'שתייה',
'חיי_לילה': 'שתייה',
'שתייה_וחיי_לילה': 'שתייה',
'לינה': 'לינה',
'בתי_מלון': 'לינה',
'חשוב_לדעת': 'רשומה',
'אוכל_ושתייה': 'אוכל'
};
// 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 = ['#Cities', '#Other_destinations', '#Islands', '#print-districts' ];
// --------------------------------------------------------------------
// CONFIGURE THE FOLLOWING TO MATCH THE LISTING TEMPLATE PARAMS & OUTPUT
// --------------------------------------------------------------------
// name of the generic listing template to use when a more specific
// template ("see", "do", etc) is not appropriate
var DEFAULT_LISTING_TEMPLATE = 'רשומה';
var LISTING_TYPE_PARAMETER = 'סוג';
var LISTING_CONTENT_PARAMETER = 'תיאור';
// 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.
// - newline: Append a newline after the parameter in the listing
// template syntax when the article is saved.
var LISTING_TEMPLATE_PARAMETERS = {
'סוג': { id:'input-type', hideDivIfEmpty: 'div_type', newline: true },
'שם': { id:'input-name' },
'שם חלופי': { id:'input-alt' },
'האתר הרשמי': { id:'input-url' },
'מייל': { id:'input-email', newline: true },
'כתובת': { id:'input-address' },
'lat': { id:'input-lat' },
'long': { id:'input-long' },
'הוראות': { id:'input-directions', newline: true },
'טלפון': { id:'input-phone' },
'שיחה בחינם': { id:'input-tollfree' },
'פקס': { id:'input-fax', hideDivIfEmpty: 'div_fax'},
'פייסבוק': { id:'input-facebook', newline: true },
'שעות': { id:'input-hours' },
"צ'ק-אין": { id:'input-checkin', hideDivIfEmpty: 'div_checkin', skipIfEmpty: true },
"צ'ק-אאוט": { id:'input-checkout', hideDivIfEmpty: 'div_checkout', skipIfEmpty: true },
'מחיר': { id:'input-price', newline: true },
'ויקיפדיה': { id:'input-wikipedia', skipIfEmpty: true },
'ויקינתונים': { id:'input-wikidata-value', skipIfEmpty: true },
'תמונה': { id:'input-image', newline: true, skipIfEmpty: true },
'מאפיינים נוספים': { id:'input-additionalfeatures', newline: true, skipIfEmpty: true },
'עודכן_לאחרונה': { id:'input-lastedit', newline: true, skipIfEmpty: true },
'תיאור': { id:'input-content', newline: true }
};
// override the default settings for "sleep" listings since that
// listing type uses "checkin"/"checkout" instead of "hours"
var SLEEP_TEMPLATE_PARAMETERS = $.extend(true, {}, LISTING_TEMPLATE_PARAMETERS, {
'שעות': { hideDivIfEmpty: 'div_hours', skipIfEmpty: true },
"צ'ק-אין": { hideDivIfEmpty: null, skipIfEmpty: false },
"צ'ק-אאוט": { hideDivIfEmpty: null, skipIfEmpty: false }
});
// local additional features
var LISTING_ADDITIONAL_FEATURES = {
'רשומה מובילה בטריפאדוויזר': { id:'istripadvisor', typesToShow: [] },
'אתר מורשת עולמית': { id:'isunesco', typesToShow: ['מוקדי', 'פעילויות', 'יישוב מרכזי'] },
'נגישות': { id:'isaccessibility', typesToShow: [''] },
'אינטרנט אלחוטי': { id:'iswifi', typesToShow: [''] },
'אש': { id:'iscampfire', typesToShow: ['פעילויות'] },
'כשר': { id:'iskosher', typesToShow: ['אוכל'] },
'צמחוני': { id:'isvegetarian' ,typesToShow: ['אוכל'] },
'חלאל': { id:'ishalal', typesToShow: ['אוכל'] },
'גיי פרנדלי': { id:'islgbt', typesToShow: ['שתייה'] },
};
// map the template name to configuration information needed by the listing
// editor
var LISTING_TEMPLATES = {
'רשומה': LISTING_TEMPLATE_PARAMETERS,
'מוקדי': LISTING_TEMPLATE_PARAMETERS,
'פעילויות': LISTING_TEMPLATE_PARAMETERS,
'בידור': LISTING_TEMPLATE_PARAMETERS,
'קניות': LISTING_TEMPLATE_PARAMETERS,
'אוכל': LISTING_TEMPLATE_PARAMETERS,
'שתייה': LISTING_TEMPLATE_PARAMETERS,
'לינה': SLEEP_TEMPLATE_PARAMETERS,
'יישוב מרכזי': LISTING_TEMPLATE_PARAMETERS,
'יעד מרכזי': LISTING_TEMPLATE_PARAMETERS
};
// --------------------------------------------------------------------
// CONFIGURE THE FOLLOWING TO IMPLEMENT THE UI FOR THE LISTING EDITOR
// --------------------------------------------------------------------
// these selectors should match a value defined in the EDITOR_FORM_HTML
// if the selector refers to a field that is not used by a Wikivoyage
// language version the variable should still be defined, but the
// corresponding element in EDITOR_FORM_HTML can be removed and thus
// the selector will not match anything and the functionality tied to
// the selector will never execute.
var EDITOR_FORM_SELECTOR = '#listing-editor';
var EDITOR_CLOSED_SELECTOR = '#input-closed';
var EDITOR_SUMMARY_SELECTOR = '#input-summary';
var EDITOR_MINOR_EDIT_SELECTOR = '#input-minor';
// the below HTML is the UI that will be loaded into the listing editor
// dialog box when a listing is added or edited. EACH WIKIVOYAGE
// LANGUAGE SITE CAN CUSTOMIZE THIS HTML - fields can be removed,
// added, displayed differently, etc. Note that it is important that
// any changes to the HTML structure are also made to the
// LISTING_TEMPLATES parameter arrays since that array provides the
// mapping between the editor HTML and the listing template fields.
var EDITOR_FORM_HTML = '<form id="listing-editor">' +
'<div class="listing-col listing-span_1_of_2">' +
'<table class="editor-fullwidth">' +
'<tr id="div_name">' +
'<td class="editor-label-col"><label for="input-name">שם</label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="שם המקום" id="input-name"></td>' +
'</tr>' +
'<tr id="div_alt">' +
'<td class="editor-label-col"><label for="input-alt">שם חלופי</label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="שם בשפת המקור או שם נוסף" id="input-alt"></td>' +
'</tr>' +
'<tr id="div_url">' +
'<td class="editor-label-col"><label for="input-url">האתר הרשמי<span class="wikidata-update"></span></label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="http://www.example.com" id="input-url" dir=ltr></td>' +
'</tr>' +
'<tr id="div_facebook">' +
'<td class="editor-label-col"><label for="input-facebook">פייסבוק<span class="wikidata-update"></span></label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="http://www.facebook.com/example" id="input-facebook" dir=ltr></td>' +
'</tr>' +
'<tr id="div_address">' +
'<td class="editor-label-col"><label for="input-address">כתובת</label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="הכתובת של המקום" id="input-address"></td>' +
'</tr>' +
'<tr id="div_directions">' +
'<td class="editor-label-col"><label for="input-directions">הוראות הגעה</label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="איך ניתן להגיע למקום?" id="input-directions"></td>' +
'</tr>' +
'<tr id="div_phone">' +
'<td class="editor-label-col"><label for="input-phone">טלפון</label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="+55 555 555-5555" id="input-phone" dir=ltr></td>' +
'</tr>' +
'<tr id="div_tollfree">' +
'<td class="editor-label-col"><label for="input-tollfree">שיחה בחינם</label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="+1-800-100-1000" id="input-tollfree" dir=ltr></td>' +
'</tr>' +
'<tr id="div_fax">' +
'<td class="editor-label-col"><label for="input-fax">פקס</label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="+55 555 555-555" id="input-fax" dir=ltr></td>' +
'</tr>' +
'<tr id="div_email">' +
'<td class="editor-label-col"><label for="input-email">מייל<span class="wikidata-update"></span></label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="hello@example.com" id="input-email" dir=ltr></td>' +
'</tr>' +
'<tr id="div_lastedit" style="display: none;">' +
'<td class="editor-label-col"><label for="input-lastedit">עודכן לאחרונה</label></td>' +
'<td><input type="text" size="10" placeholder="2015-01-15" id="input-lastedit"></td>' +
'</tr>' +
'<tr id="div_wikidata_update" style="display: none">' +
'<td class="editor-label-col"> </td>' +
'<td><span class="wikidata-update"></span><a href="javascript:" id="wikidata-shared">עדכון שדות המידע על בסיס נתונים עדכניים מוויקינתונים</a><small> <a href="javascript:" title="This simply gets the values from Wikidata and replaces the local values. Useful for new listings." class="listing-tooltip" id="wikidata-shared-quick">(quick fetch)</a></small></td>' +
'</tr>' +
'</table>' +
'</div>' +
'<div class="listing-col listing-span_1_of_2">' +
'<table class="editor-fullwidth">' +
'<tr id="div_type">' +
'<td class="editor-label-col"><label for="input-type">Type</label></td>' +
'<td>' +
'<select id="input-type" placeholder="type of listing">' +
'<option value="listing">listing</option>' +
'<option value="see">see</option>' +
'<option value="do">do</option>' +
'<option value="buy">buy</option>' +
'<option value="eat">eat</option>' +
'<option value="drink">drink</option>' +
'<option value="sleep">sleep</option>' +
'</select>' +
'</td>' +
'</tr>' +
'<tr id="div_lat">' +
'<td class="editor-label-col"><label for="input-lat">Latitude<span class="wikidata-update"></span></label></td>' +
'<td><input type="text" class="editor-partialwidth" placeholder="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="//tools.wmflabs.org/wikivoyage/w/geomap.php">find on map</a></td>' +
'</tr>' +
'<tr id="div_long">' +
'<td class="editor-label-col"><label for="input-long">Longitude<span class="wikidata-update"></span></label></td>' +
'<td><input type="text" class="editor-partialwidth" placeholder="111.11111" id="input-long">' +
' <a id="coords-null-link" href="javascript:">∅</a>' +
'</tr>' +
'<tr id="div_hours">' +
'<td class="editor-label-col"><label for="input-hours">Hours</label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="9AM-5PM or 09:00-17:00" id="input-hours"></td>' +
'</tr>' +
'<tr id="div_checkin">' +
'<td class="editor-label-col"><label for="input-checkin">Check-in</label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="check in time" id="input-checkin"></td>' +
'</tr>' +
'<tr id="div_checkout">' +
'<td class="editor-label-col"><label for="input-checkout">Check-out</label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="check out time" id="input-checkout"></td>' +
'</tr>' +
'<tr id="div_price">' +
'<td class="editor-label-col"><label for="input-price">Price</label></td>' +
'<td>' +
// update the ListingEditor.Callbacks.initCurrencySymbolFormFields
// method if the currency symbols are removed or modified
'<input type="text" class="editor-partialwidth" placeholder="entry or service price" id="input-price">' +
'<span id="span_currency">' +
'<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:">\u20AC</a></span>' +
'<span class="currency-signs"> <a href="javascript:">\u00A5</a></span>' +
'<span class="currency-signs"> <a href="javascript:">\u20A9</a></span>' +
'</span>' +
'</td>' +
'</tr>' +
'<tr id="div_wikidata">' +
'<td class="editor-label-col"><label for="input-wikidata-label">Wikidata</label></td>' +
'<td>' +
'<input type="text" class="editor-partialwidth" placeholder="wikidata record" id="input-wikidata-label">' +
'<input type="hidden" id="input-wikidata-value">' +
'<a href="javascript:" id="wp-wd" title="Get ID from Wikipedia article" style="display:none"><small> WP</small></a>' +
'<span id="wikidata-value-display-container" style="display:none">' +
'<small>' +
' <span id="wikidata-value-link"></span>' +
' | <a href="javascript:" id="wikidata-remove" title="Delete the Wikidata entry from this listing">remove</a>' +
'</small>' +
'</span>' +
'</td>' +
'</tr>' +
'<tr id="div_wikipedia">' +
'<td class="editor-label-col"><label for="input-wikipedia">Wikipedia<span class="wikidata-update"></span></label></td>' +
'<td>' +
'<input type="text" class="editor-partialwidth" placeholder="wikipedia article" id="input-wikipedia">' +
'<span id="wikipedia-value-display-container" style="display:none">' +
'<small>' +
' <span id="wikipedia-value-link"></span>' +
'</small>' +
'</span>' +
'</td>' +
'</tr>' +
'<tr id="div_image">' +
'<td class="editor-label-col"><label for="input-image">Image<span class="wikidata-update"></span></label></td>' +
'<td>' +
'<input type="text" class="editor-partialwidth" placeholder="image of place" id="input-image">' +
'<span id="image-value-display-container" style="display:none">' +
'<small>' +
' <span id="image-value-link"></span>' +
'</small>' +
'</span>' +
'</td>' +
'</tr>' +
'</table>' +
'</div>' +
'<table class="editor-fullwidth">' +
'<tr id="div_content">' +
'<td class="editor-label-col"><label for="input-content">Content</label></td>' +
'<td><textarea rows="8" class="editor-fullwidth" placeholder="description of place" id="input-content"></textarea></td>' +
'</tr>' +
// update the ListingEditor.Callbacks.hideEditOnlyFields method if
// the status row is removed or modified
'<tr id="div_status">' +
'<td class="editor-label-col"><label>Status</label></td>' +
'<td>' +
'<span id="span-closed">' +
'<input type="checkbox" id="input-closed">' +
'<label for="input-closed" class="listing-tooltip" title="Check the box if the business is no longer in operation or if the listing should be deleted for some other reason, and it will be removed from this article">delete this listing?</label>' +
'</span>' +
// update the ListingEditor.Callbacks.updateLastEditDate
// method if the last edit input is removed or modified
'<span id="span-last-edit">' +
'<input type="checkbox" id="input-last-edit" />' +
'<label for="input-last-edit" class="listing-tooltip" title="Check the box if the information in this listing has been verified to be current and accurate, and the last updated date will be changed to the current date">mark the listing as up-to-date?</label>' +
'</span>' +
'</td>' +
'</tr>' +
'</table>' +
// update the ListingEditor.Callbacks.hideEditOnlyFields method if
// the summary table is removed or modified
'<table class="editor-fullwidth" id="div_summary">' +
'<tr><td colspan="2"><div class="listing-divider" /></td></tr>' +
'<tr>' +
'<td class="editor-label-col"><label for="input-summary">Edit Summary</label></td>' +
'<td>' +
'<input type="text" class="editor-partialwidth" placeholder="reason listing was changed" id="input-summary">' +
'<span id="span-minor"><input type="checkbox" id="input-minor"><label for="input-minor" class="listing-tooltip" title="Check the box if the change to the listing is minor, such as a typo correction">minor change?</label></span>' +
'</td>' +
'</tr>' +
'</table>' +
'<div id="listing-preview" style="display: none;">' +
'<div class="listing-divider"></div>' +
'<div class="editor-row">' +
'<div title="Preview">Preview</div>' +
'<div><div id="listing-preview-text"></div></div>' +
'</div>' +
'</div>' +
'</form>';
// expose public members
return {
LANG: LANG,
WIKIDATAID: WIKIDATAID,
COMMONS_URL: COMMONS_URL,
WIKIDATA_URL: WIKIDATA_URL,
WIKIDATA_CLAIMS: WIKIDATA_CLAIMS,
WIKIPEDIA_URL: WIKIPEDIA_URL,
WIKIDATA_SITELINK_WIKIPEDIA: WIKIDATA_SITELINK_WIKIPEDIA,
TRANSLATIONS: TRANSLATIONS,
MAX_DIALOG_WIDTH: MAX_DIALOG_WIDTH,
DISALLOW_ADD_LISTING_IF_PRESENT: DISALLOW_ADD_LISTING_IF_PRESENT,
DEFAULT_LISTING_TEMPLATE: DEFAULT_LISTING_TEMPLATE,
LISTING_TYPE_PARAMETER: LISTING_TYPE_PARAMETER,
LISTING_CONTENT_PARAMETER: LISTING_CONTENT_PARAMETER,
EDIT_LINK_CONTAINER_SELECTOR: EDIT_LINK_CONTAINER_SELECTOR,
ALLOW_UNRECOGNIZED_PARAMETERS: ALLOW_UNRECOGNIZED_PARAMETERS,
LOCALLY_SYNC_COORDS: LOCALLY_SYNC_COORDS,
SECTION_TO_TEMPLATE_TYPE: SECTION_TO_TEMPLATE_TYPE,
LISTING_TEMPLATES: LISTING_TEMPLATES,
EDITOR_FORM_SELECTOR: EDITOR_FORM_SELECTOR,
EDITOR_CLOSED_SELECTOR: EDITOR_CLOSED_SELECTOR,
EDITOR_SUMMARY_SELECTOR: EDITOR_SUMMARY_SELECTOR,
EDITOR_MINOR_EDIT_SELECTOR: EDITOR_MINOR_EDIT_SELECTOR,
EDITOR_FORM_HTML: EDITOR_FORM_HTML
};
}();
/* ***********************************************************************
* ListingEditor.Callbacks implements custom functionality that may be
* specific to how a Wikivoyage language version has implemented the
* listing template. For example, English Wikivoyage uses a "last edit"
* date that needs to be populated when the listing editor form is
* submitted, and that is done via custom functionality implemented as a
* SUBMIT_FORM_CALLBACK function in this module.
* ***********************************************************************/
ListingEditor.Callbacks = function() {
// array of functions to invoke when creating the listing editor form.
// these functions will be invoked with the form DOM object as the
// first element and the mode as the second element.
var CREATE_FORM_CALLBACKS = [];
// array of functions to invoke when submitting the listing editor
// form but prior to validating the form. these functions will be
// invoked with the mapping of listing attribute to value as the first
// element and the mode as the second element.
var SUBMIT_FORM_CALLBACKS = [];
// array of validation functions to invoke when the listing editor is
// submitted. these functions will be invoked with an array of
// validation messages as an argument; a failed validation should add a
// message to this array, and the user will be shown the messages and
// the form will not be submitted if the array is not empty.
var VALIDATE_FORM_CALLBACKS = [];
// --------------------------------------------------------------------
// LISTING EDITOR UI INITIALIZATION CALLBACKS
// --------------------------------------------------------------------
/**
* Add listeners to the currency symbols so that clicking on a currency
* symbol will insert it into the price input.
*/
var initCurrencySymbolFormFields = function(form, mode) {
var CURRENCY_SIGNS_SELECTOR = '.currency-signs';
$(CURRENCY_SIGNS_SELECTOR, form).click(function() {
var priceInput = $('#input-price');
var caretPos = priceInput[0].selectionStart;
var oldPrice = priceInput.val();
var currencySymbol = $(this).find('a').text();
var newPrice = oldPrice.substring(0, caretPos) + currencySymbol + oldPrice.substring(caretPos);
priceInput.val(newPrice);
});
};
CREATE_FORM_CALLBACKS.push(initCurrencySymbolFormFields);
/**
* Add listeners on various fields to update the "find on map" link.
*/
var initFindOnMapLink = function(form, mode) {
var latlngStr = '?lang=' + ListingEditor.Config.LANG;
latlngStr += '&page=' + encodeURIComponent(mw.config.get('wgTitle'));
// #geodata should be a hidden span added by Template:Geo
// containing the lat/long coordinates of the destination
if ($('#geodata').length) {
var latlng = $('#geodata').text().split('; ');
latlngStr += '&lat=' + latlng[0] + '&lon=' + latlng[1] + '&zoom=15';
}
if ($('#input-address', form).val() !== '') {
latlngStr += '&location=' + encodeURIComponent($('#input-address', form).val());
} else if ($('#input-name', form).val() !== '') {
latlngStr += '&location=' + encodeURIComponent($('#input-name', form).val());
}
// #geomap-link is a link in the EDITOR_FORM_HTML
$('#geomap-link', form).attr('href', $('#geomap-link', form).attr('href') + latlngStr);
$('#input-address', form).change( function () {
var link = $('#geomap-link').attr('href');
var index = link.indexOf('&location');
if (index < 0) index = link.length;
$('#geomap-link').attr('href', link.substr(0,index) + '&location='
+ encodeURIComponent($('#input-address').val()));
});
};
CREATE_FORM_CALLBACKS.push(initFindOnMapLink);
var hideEditOnlyFields = function(form, mode) {
var EDITOR_STATUS_ROW = '#div_status';
var EDITOR_SUMMARY_ROW = '#div_summary';
if (mode !== ListingEditor.Core.MODE_EDIT) {
$(EDITOR_STATUS_ROW, form).hide();
$(EDITOR_SUMMARY_ROW, form).hide();
}
};
CREATE_FORM_CALLBACKS.push(hideEditOnlyFields);
var typeToColor = function(listingType, form) {
$('#input-type', form).css( 'box-shadow', 'unset' );
$.ajax ({
'listingType': listingType,
'form': form,
url: mw.config.get('wgScriptPath') + '/api.php?' + $.param({
action: 'parse',
prop: 'text',
contentmodel: 'wikitext',
format: 'json',
disablelimitreport: true,
'text': '{{#invoke:TypeToColor|convert|'+listingType+'}}',
}),
beforeSend: function() {
if (localStorage.getItem('listing-'+listingType)) {
changeColor(localStorage.getItem('listing-'+listingType), form);
return false;
}
else { return true; }
},
success: function (data) {
var color = $(data.parse.text['*']).text().trim();
localStorage.setItem('listing-'+listingType, color);
changeColor(color, form);
},
});
};
var changeColor = function(color, form) {
$('#input-type', form).css( 'box-shadow', '-20px 0 0 0 #' + color + ' inset' );
}
var initColor = function(form, mode) {
typeToColor( $('#input-type', form).val(), form );
$('#input-type', form).on('change', function () {
typeToColor(this.value, form);
});
};
CREATE_FORM_CALLBACKS.push(initColor);
var isRTL = function (s){ // based on https://stackoverflow.com/questions/12006095/javascript-how-to-check-if-character-is-rtl
var ltrChars = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF'+'\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF',
rtlChars = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC',
rtlDirCheck = new RegExp('^[^'+ltrChars+']*['+rtlChars+']');
return rtlDirCheck.test(s);
};
var autoDir = function(selector) {
if (selector.val() && !isRTL(selector.val())) {
selector.prop('dir', 'ltr');
}
selector.keyup(function() {
if (isRTL(selector.val()) || !selector.val() ) {
selector.prop('dir', 'rtl');
}
else {
selector.prop('dir', 'ltr');
}
});
};
var autoDirParameters = function(form) {
autoDir($('#input-alt', form));
};
CREATE_FORM_CALLBACKS.push(autoDirParameters);
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 = "";
}
$("#input-wikidata-label").val(label);
};
ListingEditor.SisterSite.ajaxSisterSiteSearch(ajaxUrl, ajaxData, ajaxSuccess);
}
// 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);
}
}).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();
};
$('#wp-wd', form).click(function() {
var wikipediaLink = $("#input-wikipedia", form).val();
getWikidataFromWikipedia(wikipediaLink, form);
});
$('#wikidata-shared', form).click(function() {
var wikidataRecord = $("#input-wikidata-value", form).val();
updateWikidataSharedFields(wikidataRecord);
});
$('#wikidata-shared-quick', form).click(function() {
var wikidataRecord = $("#input-wikidata-value", form).val();
quickUpdateWikidataSharedFields(wikidataRecord);
});
$('#coords-null-link', form).click(function() {
$("#input-lat", form).val("NA");
$("#input-long", form).val("NA");
});
var wikipediaSiteData = {
apiUrl: ListingEditor.SisterSite.API_WIKIPEDIA,
selector: $('#input-wikipedia', form),
form: form,
ajaxData: {
namespace: 0
},
updateLinkFunction: wikipediaLink
};
ListingEditor.SisterSite.initializeSisterSiteAutocomplete(wikipediaSiteData);
var commonsSiteData = {
apiUrl: ListingEditor.SisterSite.API_COMMONS,
selector: $('#input-image', form),
form: form,
ajaxData: {
namespace: 6
},
updateLinkFunction: commonsLink
};
ListingEditor.SisterSite.initializeSisterSiteAutocomplete(commonsSiteData);
};
var wikipediaLink = function(value, form) {
var wikipediaSiteLinkData = {
inputSelector: '#input-wikipedia',
containerSelector: '#wikipedia-value-display-container',
linkContainerSelector: '#wikipedia-value-link',
href: ListingEditor.Config.WIKIPEDIA_URL + '/wiki/' + mw.util.wikiUrlencode(value),
linkTitle: ListingEditor.Config.TRANSLATIONS.viewWikipediaPage
};
sisterSiteLinkDisplay(wikipediaSiteLinkData, form);
$("#wp-wd", form).show();
if ( value === '' ) { $("#wp-wd").hide(); }
};
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, placeholderBool) {
var selectorName = selector.replace(/lat|long/, 'coords');
if ( value ) { // not null
if ( placeholderBool !== true ) {
$(selector).val(value);
} else {
$(selector).val('').attr('placeholder', value).attr('disabled', true);
}
}
};
var quickUpdateWikidataSharedFields = 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 === 'coords') {
res[key].latitude = ListingEditor.Core.trimDecimal(res[key].latitude, 6);
res[key].longitude = ListingEditor.Core.trimDecimal(res[key].longitude, 6);
msg += '\n' + ListingEditor.Config.WIKIDATA_CLAIMS[key].label + ': ' + res[key].latitude + ' ' + res[key].longitude;
}
else if (key === 'iata') {
msg += '\n' + ListingEditor.Config.WIKIDATA_CLAIMS[key].label + ': ' + res[key];
res[key] = '{{IATA|'+res[key]+'}}';
}
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];
}
}
var wikipedia = ListingEditor.SisterSite.wikidataWikipedia(jsonObj, wikidataRecord);
if (wikipedia) {
msg += '\n' + ListingEditor.Config.TRANSLATIONS.sharedWikipedia + ': ' + wikipedia;
}
if (msg) {
if (confirm(ListingEditor.Config.TRANSLATIONS.wikidataShared + '\n' + msg)) {
for (key in res) {
if (res[key]) {
if (key === 'coords') {
updateFieldIfNotNull('#input-lat', res[key].latitude, !ListingEditor.Config.LOCALLY_SYNC_COORDS);
updateFieldIfNotNull('#input-long', res[key].longitude, !ListingEditor.Config.LOCALLY_SYNC_COORDS);
}
else if (key === 'iata') {
if ( $('#input-alt').val() === '' || /^{{IATA\|...}}$/.test($('#input-alt').val()) ) {
// if Alt is empty or only contains an IATA code
updateFieldIfNotNull('#input-'+ListingEditor.Config.WIKIDATA_CLAIMS[key].fields[0], res[key]);
}
}
else {
updateFieldIfNotNull('#input-'+ListingEditor.Config.WIKIDATA_CLAIMS[key].fields[0], res[key]);
if (key === 'image') { commonsLink(res[key]); }
}
}
}
updateFieldIfNotNull('#input-wikipedia', wikipedia);
if (wikipedia) {
wikipediaLink(wikipedia);
}
}
} else {
alert(ListingEditor.Config.TRANSLATIONS.wikidataSharedNotFound);
}
};
ListingEditor.SisterSite.ajaxSisterSiteSearch(ajaxUrl, ajaxData, ajaxSuccess);
};
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 = '<form id="listing-editor-sync">' +
ListingEditor.Config.TRANSLATIONS.wikidataSyncBlurb +
'<p>' +
'<fieldset style="border:none;padding:0">' +
'<span class="listing-span_1_of_2">' +
'<span class="wikidata-update" />' +
'<a href="javascript:" class="syncSelect" name="wd" title="' + ListingEditor.Config.TRANSLATIONS.selectAll + '"><h4>Wikidata</h4></a>' +
'</span>' +
'<span class="listing-span_1_of_2">' +
'<a href="javascript:" class="syncSelect" name="wv" title="' + ListingEditor.Config.TRANSLATIONS.selectAll + '"><h4>Wikivoyage</h4></a>' +
'<span class="wikivoyage-update" />' +
'</span>' +
'</fieldset><a href="javascript:" id="autoSelect" class="listing-tooltip" title="Select all values where the alternative is empty.">Auto</a>';
//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] = {};
res[key].value = ListingEditor.SisterSite.wikidataClaim(jsonObj, wikidataRecord, ListingEditor.Config.WIKIDATA_CLAIMS[key].p);
res[key].guidObj = ListingEditor.SisterSite.wikidataClaim(jsonObj, wikidataRecord, ListingEditor.Config.WIKIDATA_CLAIMS[key].p, true);
if ( (key === 'lat') || (key === 'long') ) {
/* if( res[key].value ) {
msg += createRadio(ListingEditor.Config.WIKIDATA_CLAIMS[key], res[key].value[key+'itude'], res[key].guidObj);
} */
}
else if (key === 'iata') {
if( res[key].value ) { res[key].value = '{{IATA|'+res[key].value+'}}'; }
msg += createRadio(ListingEditor.Config.WIKIDATA_CLAIMS[key], [res[key].value], res[key].guidObj);
}
else if (key === 'email') {
if( res[key].value ) { res[key].value = res[key].value.replace('mailto:', ''); }
msg += createRadio(ListingEditor.Config.WIKIDATA_CLAIMS[key], [res[key].value], res[key].guidObj);
}
else if (key === 'coords') {
if ( res[key].value ) { msg += createRadio(ListingEditor.Config.WIKIDATA_CLAIMS[key], [res[key].value.latitude, res[key].value.longitude], res[key].guidObj); }
else { msg += createRadio(ListingEditor.Config.WIKIDATA_CLAIMS[key], [res[key].value], res[key].guidObj); }
}
else msg += createRadio(ListingEditor.Config.WIKIDATA_CLAIMS[key], [res[key].value], res[key].guidObj);
}
var wikipedia = ListingEditor.SisterSite.wikidataWikipedia(jsonObj, wikidataRecord);
msg += createRadio({'label': ListingEditor.Config.TRANSLATIONS.sharedWikipedia, 'fields': ['wikipedia'], 'doNotUpload': true,}, [wikipedia], $('#input-wikidata-value').val());
msg += '<p><small><a href="javascript:" class="clear" title="all">clear all</a></small>';
msg += '</form>';
// copied from dialog above. ideally should be global variable @TODO
var windowWidth = $(window).width();
var dialogWidth = (windowWidth > ListingEditor.Config.MAX_DIALOG_WIDTH) ? (0.85*ListingEditor.Config.MAX_DIALOG_WIDTH) : 'auto';
if($(msg).find('fieldset').length > 1) { // if no radio pairs generated. 1 guaranteed fieldset for the column headings
$(msg).dialog({
title: ListingEditor.Config.TRANSLATIONS.syncTitle,
width: dialogWidth,
dialogClass: 'listing-editor-dialog',
buttons: [
{
text: ListingEditor.Config.TRANSLATIONS.submit,
click: function() {
$('#listing-editor-sync').find('input:radio:checked').each(function () {
var label = $('label[for="'+ $(this).attr('id') +'"]');
var syncedValue = label.text().split('\n');
var field = JSON.parse($(this).parents('fieldset').find('.listing-span_1_of_2:nth-of-type(2) > input:hidden').val());
var editorField = [];
for( var i = 0; i < field.fields.length; i++ ) { editorField[i] = '#'+ListingEditor.Config.LISTING_TEMPLATES.listing[field.fields[i]].id; }
var guidObj = $(this).parents('fieldset').find('.listing-span_1_of_2:nth-of-type(1) > input:hidden').val();
if ( field.p === 'P625' ) { //coords, first latitude, then longitude
if ( ListingEditor.Config.LOCALLY_SYNC_COORDS === true ) {
for ( i = 0; i < editorField.length; i++) { $(editorField[i]).val(ListingEditor.Core.trimDecimal(Number(syncedValue[i]), 6)); }
} else {
for ( i = 0; i < editorField.length; i++) {
$(editorField[i]).val('').prop('disabled', true).attr('placeholder', ListingEditor.Core.trimDecimal(Number(syncedValue[i]), 6));
}
}
// @TODO: make the find on map link work for placholder coords
var precision = Math.min(syncedValue[0].replace(/\d/g, "0").replace(/$/, "1"), syncedValue[1].replace(/\d/g, "0").replace(/$/, "1"));
syncedValue = '{ "latitude":' + syncedValue[0] + ', "longitude": ' + syncedValue[1] + ', "precision": ' + precision + ' };';
// syncedValue should not be an array for any other field
}
else {
syncedValue = syncedValue[0]; // remove dummy newline
$(editorField[0]).val(syncedValue);
if( field.p === 'P968' ) { syncedValue = 'mailto:'+syncedValue; } //email
syncedValue = '"'+syncedValue+'"';
}
var ajaxUrl = ListingEditor.SisterSite.API_WIKIDATA;
var ajaxData = {
action: 'wbgetentities',
ids: field.p,
props: 'datatype',
};
var ajaxSuccess = function(jsonObj) {
//if ( @TODO: add logic for detecting Wikipedia and not doing this test. Otherwise get an error trying to find undefined. Keep in mind that we would in the future call sitelink changing here maybe. Not urgent, error harmless ) { }
/*else*/ if ( jsonObj.entities[field.p].datatype === 'monolingualtext' ) { syncedValue = '{"text": ' + syncedValue + ', "language": "' + ListingEditor.Config.LANG + '"}'; }
if ( guidObj === "null" ) { // no value on Wikidata, string "null" gets saved in hidden field. There should be no cases in which there is no Wikidata item but this string does not equal "null"
ListingEditor.SisterSite.sendToWikidata(field.p , syncedValue, 'value');
}
else {
if ( syncedValue === "" ) {
ListingEditor.SisterSite.removeFromWikidata(guidObj);
}
else {
// this is changing, for when guid is not null and neither is the value
// Wikidata silently ignores a request to change a value to its existing value
ListingEditor.SisterSite.changeOnWikidata(guidObj, field.p, syncedValue, 'value');
}
}
};
if( (field.doNotUpload !== true) && ($(this).attr('id').search(/-wd$/) === -1) ) { // -1: regex not found
ListingEditor.SisterSite.ajaxSisterSiteSearch(ajaxUrl, ajaxData, ajaxSuccess);
}
});
// @TODO: make sure closing the original listing editor dialog also closes this one
// @TODO: after testing is done, remove all console.log statements, do something about errors. alert? ignore?
$(this).dialog('close');
}
},
{
text: ListingEditor.Config.TRANSLATIONS.cancel, click: function() {
$(this).dialog('close');
}
},
],
open: function() {
$('#div_wikidata_update').hide();
$('#wikidata-remove').hide();
$('#input-wikidata-label').prop('disabled', true);
},
close: function() {
$('#div_wikidata_update').show();
$('#wikidata-remove').show();
$('#input-wikidata-label').prop('disabled', false);
document.getElementById("listing-editor-sync").outerHTML = ""; // delete the dialog. Direct DOM manipulation so the model gets updated. This is to avoid issues with subsequent dialogs no longer matching labels with inputs because IDs are already in use.
}
});
} else {
alert(ListingEditor.Config.TRANSLATIONS.wikidataSharedNotFound); // @TODO: change this to explain that everything matches
}
wikidataLink("", $("#input-wikidata-value").val()); // called to append the Wikidata link to the dialog title
$('.clear').click( function() {
var field = '#' + $(this).attr('title');
if ( field === '#all' ) {
$('#listing-editor-sync').find('input:radio:checked').prop('checked', false);
return false;
}
$(field+'-wv').prop('checked', false);
$(field+'-wd').prop('checked', false);
});
$('.syncSelect').click( function() {
var field = $(this).attr('name'); // wv or wd
$('#listing-editor-sync input[type=radio]').prop('checked', false);
$('#listing-editor-sync input[id$='+field).prop('checked', true);
});
$('#autoSelect').click( function() { // auto select non-empty values
$('#listing-editor-sync').find('label').each(function () {
if (!$(this).text().trim().length) { // if a label has no text
var emptyValue = $(this).attr('for');
$(this).parents('fieldset').find('input:radio:not(#'+emptyValue+')').prop('checked', true); // select the other button in the pair
// there should never be a situation in which neither label has text, but this should just select one of them if so
}
});
});
};
ListingEditor.SisterSite.ajaxSisterSiteSearch(ajaxUrl, ajaxData, ajaxSuccess);
};
var createRadio = function(field, claimValue, guid) {
var j = 0;
for (j = 0; j < claimValue.length; j++) { if( claimValue[j] === null ) { claimValue[j] = ''; } }
field.label = field.label.split(/(\s+)/)[0]; // take first word
var html = '';
var editorField = [];
for ( var i = 0; i < field.fields.length; i++ ) { editorField[i] = '#'+ListingEditor.Config.LISTING_TEMPLATES.listing[field.fields[i]].id; }
// NOTE: this assumes a standard listing type. If ever a field in a nonstandard listing type is added to Wikidata sync, this must be changed
for (j = 0; j < claimValue.length; j++) {
if ( field.p === 'P625' && $(editorField[j]).val() == 'NA' ) { field.doNotUpload = true; } // do not upload NA coordinates
if ( ( field.p === 'P625' ? ListingEditor.Core.trimDecimal(Number(claimValue[j]), 6) : claimValue[j] ) != $(editorField[j]).val() ) { break; }
// compare the present value to the Wikidata value. If coords, then trim the WD decimal before comparing
}
if ( j === claimValue.length ) { return ''; }
// if everything on WV equals everything on WD, skip this field
if ( (field.doNotUpload === true) && (claimValue[0] === '') ) { return ''; }
// if do not upload is set and there is nothing on WD, skip
// optional @TODO: do not add box if wikilink is the same link but not same string (e.g. if the link on WV has underscores instead of spaces)
if ( (field.p === 'P625' ) && ( ListingEditor.Config.LOCALLY_SYNC_COORDS !== true ) && ( $(editorField[0]).val() === '' ) && ( $(editorField[1]).val() === '' ) )
{ return '<p><i>' + ListingEditor.Config.TRANSLATIONS.wikidataRemoteCoords + '</i></p>'; }
// if coords, and they are not being locally synced, and there aren't any here, skip with a message
html += '<fieldset>' +
'<legend>' + field.label + '</legend>' +
'<span class="listing-span_1_of_2">' +
'<input type="radio" id="' + field.label + '-wd" name="' + field.label + '">' +
'<input type="hidden" value="' + guid + '">' +
'<label for="' + field.label + '-wd">';
if ( field.p === 'P625' || field.p === 'P856' ) { // coords or url
html += makeSyncLinks(claimValue, field.p, false);
}
for (j = 0; j < claimValue.length; j++) { html += claimValue[j] + '\n'; }
if ( field.p === 'P625' || field.p === 'P856' ) {
html += '</a>';
}
html += '</label>' +
'</span>' +
'<span class="listing-span_1_of_2">' +
'<label for="' + field.label + '-wv">';
if ( field.p === 'P625' || field.p === 'P856' ) {
html += makeSyncLinks(editorField, field.p, true);
}
for (i = 0; i < editorField.length; i++ ) { html += $(editorField[i]).val() + '\n'; }
if ( field.p === 'P625' || field.p === 'P856' ) {
html += '</a>';
}
html += '</label>';
if ( !(field.doNotUpload === true) ) { html += '<input type="radio" id="' + field.label + '-wv" name="' + field.label + '">'; }
html += '<input type="hidden" value=\'' + JSON.stringify(field) + '\'>' +
'</span>' +
'</fieldset>\n';
html += '<small>' +
'<a href="javascript:" class="clear" title="' + field.label + '">clear</a>' +
'</small>';
return html;
};
var makeSyncLinks = function(value, mode, valBool) {
var htmlPart = '<a target="_blank" rel="noopener noreferrer" href="';
var i;
switch(mode) {
case 'P625':
htmlPart += 'https://tools.wmflabs.org/geohack/geohack.php?params=';
for (i = 0; i < value.length; i++) { htmlPart += (valBool ? $(value[i]).val() : value[i]) + ';'; }
htmlPart += '_type:landmark">'; // sets the default zoom
break;
case 'P856':
for (i = 0; i < value.length; i++) { htmlPart += (valBool ? $(value[i]).val() : value[i]); }
htmlPart += '">';
break;
}
return htmlPart;
};
var getWikidataFromWikipedia = function(wikipedia, form) {
var ajaxUrl = ListingEditor.SisterSite.API_WIKIPEDIA;
var ajaxData = {
action: 'query',
prop: 'pageprops',
ppprop: 'wikibase_item',
indexpageids: 1,
titles: wikipedia,
};
var ajaxSuccess = function (jsonObj) {
var wikidataID = ListingEditor.SisterSite.wikipediaWikidata(jsonObj);
if( wikidataID ) {
$("#input-wikidata-value").val(wikidataID);
$("#input-wikidata-label").val(wikidataID);
wikidataLink(form, wikidataID);
}
};
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();
$('#div_wikidata_update', form).show();
if ( $("#listing-editor-sync").prev().find(".ui-dialog-title").length ) { $("#listing-editor-sync").prev().find(".ui-dialog-title").append( ' — ' ).append(link.clone()); } // add to title of Wikidata sync dialog, if it is open
};
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() === '') {
validationFailureMessages.push(ListingEditor.Config.TRANSLATIONS.validationEmptyListing);
}
};
VALIDATE_FORM_CALLBACKS.push(validateListingHasData);
/**
* Implement SIMPLE validation on email addresses. Invalid emails can
* still get through, but this method implements a minimal amount of
* validation in order to catch the worst offenders.
*/
var validateEmail = function(validationFailureMessages) {
var VALID_EMAIL_REGEX = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
_validateFieldAgainstRegex(validationFailureMessages, VALID_EMAIL_REGEX, '#input-email', ListingEditor.Config.TRANSLATIONS.validationEmail);
};
VALIDATE_FORM_CALLBACKS.push(validateEmail);
/**
* Implement SIMPLE validation on Wikipedia field to verify that the
* user is entering the article title and not a URL.
*/
var validateWikipedia = function(validationFailureMessages) {
var VALID_WIKIPEDIA_REGEX = new RegExp('^(?!https?://)', 'i');
_validateFieldAgainstRegex(validationFailureMessages, VALID_WIKIPEDIA_REGEX, '#input-wikipedia', ListingEditor.Config.TRANSLATIONS.validationWikipedia);
};
VALIDATE_FORM_CALLBACKS.push(validateWikipedia);
/**
* 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 WIKIDATA_CLAIM_COORD = 'P625';
var WIKIDATA_CLAIM_LINK = 'P856';
var WIKIDATA_CLAIM_IMAGE = 'P18';
var WIKIDATA_CLAIM_IATA = 'P238';
var WIKIDATA_CLAIM_DIRNS = 'P2795';
var WIKIDATA_CLAIM_EMAIL = 'P968';
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, guidBool) {
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;
}
var index = 0;
if( propertyObj[index].mainsnak.datavalue.type === "monolingualtext" ) { // have to select correct language, Wikidata sends all despite specifying
while( propertyObj[index].mainsnak.datavalue.value.language !== ListingEditor.Config.LANG ) {
index = index + 1;
if( !(propertyObj[index]) ) { return null; } // if we run out of langs and none of them matched
}
if (guidBool === true) { return propertyObj[index].id }
return propertyObj[index].mainsnak.datavalue.value.text;
}
if (guidBool === true) { return propertyObj[index].id }
return propertyObj[index].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.en) {
return null;
}
return entityObj.labels.en.value;
};
// parse the wikipedia link from the wikidata response
var _wikidataWikipedia = function(jsonObj, value) {
var entityObj = _wikidataEntity(jsonObj, value);
if (!entityObj || !entityObj.sitelinks || !entityObj.sitelinks[ListingEditor.Config.WIKIDATA_SITELINK_WIKIPEDIA] || !entityObj.sitelinks[ListingEditor.Config.WIKIDATA_SITELINK_WIKIPEDIA].title) {
return null;
}
return entityObj.sitelinks[ListingEditor.Config.WIKIDATA_SITELINK_WIKIPEDIA].title;
};
var _wikipediaWikidata = function(jsonObj) {
if (!jsonObj || !jsonObj.query || jsonObj.query.pageids[0] == "-1" ) { // wikipedia returns -1 pageid when page is not found
return null;
}
var pageID = jsonObj.query.pageids[0];
return jsonObj['query']['pages'][pageID]['pageprops']['wikibase_item'];
};
var _sendToWikidata = function(prop, value, snaktype) {
var ajaxData = {
action: 'wbcreateclaim',
entity: $('#input-wikidata-value').val(),
property: prop,
snaktype: snaktype,
value: value,
format: 'json',
};
var ajaxSuccess = function(jsonObj) {
console.log(jsonObj);
ListingEditor.SisterSite.referenceWikidata(jsonObj);
};
var api = new mw.ForeignApi( ListingEditor.SisterSite.API_WIKIDATA );
api.postWithToken( 'csrf', ajaxData, {success: ajaxSuccess, async: false} ); // async disabled because otherwise get edit conflicts with multiple changes submitted at once
};
var _removeFromWikidata = function(guidObj) {
var ajaxData = {
action: 'wbremoveclaims',
claim: guidObj,
};
var ajaxSuccess = function(jsonObj) {
console.log(jsonObj);
};
var api = new mw.ForeignApi( ListingEditor.SisterSite.API_WIKIDATA );
api.postWithToken( 'csrf', ajaxData, {success: ajaxSuccess, async: false} );
};
var _changeOnWikidata = function(guidObj, prop, value, snaktype) {
var ajaxData = {
action: 'wbsetclaimvalue',
claim: guidObj,
snaktype: snaktype,
value: value,
};
var ajaxSuccess = function(jsonObj) {
console.log(jsonObj);
if( !(jsonObj.claim.references) ) { // if no references, add imported from
ListingEditor.SisterSite.referenceWikidata(jsonObj);
}
else if ( jsonObj.claim.references.length === 1 ) { // skip if >1 reference; too complex to automatically set
var acceptedProps = ["P143", "P4656"]; // properties relating to Wikimedia import only
var diff = $(jsonObj.claim.references[0]['snaks-order']).not(acceptedProps).get(); // x-compatible method for diff on arrays, from https://stackoverflow.com/q/1187518
if( diff.length === 0 ) { // if the set of present properties is a subset of the set of acceptable properties
ListingEditor.SisterSite.unreferenceWikidata(jsonObj.claim.id, jsonObj.claim.references[0].hash); // then remove the current reference
ListingEditor.SisterSite.referenceWikidata(jsonObj); // and add imported from
}
}
};
var api = new mw.ForeignApi( ListingEditor.SisterSite.API_WIKIDATA );
api.postWithToken( 'csrf', ajaxData, {success: ajaxSuccess, async: false} );
};
var _referenceWikidata = function(jsonObj) {
var revUrl = 'https:' + mw.config.get('wgServer') + mw.config.get('wgArticlePath').replace('$1', '') + mw.config.get('wgPageName') + '?oldid=' + mw.config.get('wgCurRevisionId'); // surprising that there is no API call for this
var ajaxData = {
action: 'wbsetreference',
statement: jsonObj.claim.id,
snaks: '{"P143":[{"snaktype":"value","property":"P143","datavalue":{"type":"wikibase-entityid","value":{"entity-type":"item","numeric-id":"' + ListingEditor.Config.WIKIDATAID + '"}}}],' +
'"P4656": [{"snaktype":"value","property":"P4656","datavalue":{"type":"string","value":"' + revUrl + '"}}]}',
};
var ajaxSuccess = function(jsonObj) {
console.log(jsonObj);
};
var api = new mw.ForeignApi( ListingEditor.SisterSite.API_WIKIDATA );
api.postWithToken( 'csrf', ajaxData, {success: ajaxSuccess, async: false} );
};
var _unreferenceWikidata = function(statement, references) {
var ajaxData = {
action: 'wbremovereferences',
statement: statement,
references: references,
};
var ajaxSuccess = function(jsonObj) {
console.log(jsonObj);
};
var api = new mw.ForeignApi( ListingEditor.SisterSite.API_WIKIDATA );
api.postWithToken( 'csrf', ajaxData, {success: ajaxSuccess, async: false} );
};
// expose public members
return {
API_WIKIDATA: API_WIKIDATA,
API_WIKIPEDIA: API_WIKIPEDIA,
API_COMMONS: API_COMMONS,
WIKIDATA_CLAIM_COORD: WIKIDATA_CLAIM_COORD,
WIKIDATA_CLAIM_LINK: WIKIDATA_CLAIM_LINK,
WIKIDATA_CLAIM_IMAGE: WIKIDATA_CLAIM_IMAGE,
WIKIDATA_CLAIM_IATA: WIKIDATA_CLAIM_IATA,
WIKIDATA_CLAIM_DIRNS: WIKIDATA_CLAIM_DIRNS,
WIKIDATA_CLAIM_EMAIL: WIKIDATA_CLAIM_EMAIL,
initializeSisterSiteAutocomplete: _initializeSisterSiteAutocomplete,
ajaxSisterSiteSearch: _ajaxSisterSiteSearch,
wikidataClaim: _wikidataClaim,
wikidataWikipedia: _wikidataWikipedia,
wikidataLabel: _wikidataLabel,
wikipediaWikidata: _wikipediaWikidata,
sendToWikidata: _sendToWikidata,
removeFromWikidata: _removeFromWikidata,
changeOnWikidata: _changeOnWikidata,
referenceWikidata: _referenceWikidata,
unreferenceWikidata: _unreferenceWikidata
};
}();
/* ***********************************************************************
* ListingEditor.Core contains code that should be shared across different
* Wikivoyage languages. This code uses the custom configurations in the
* ListingEditor.Config and ListingEditor.Callback modules to initialize
* the listing editor and process add and update requests for listings.
* ***********************************************************************/
ListingEditor.Core = function() {
var api = new mw.Api();
var MODE_ADD = 'add';
var MODE_EDIT = 'edit';
// selector that identifies the edit link as created by the
// addEditButtons() function
var EDIT_LINK_SELECTOR = '.vcard-edit-button';
var SAVE_FORM_SELECTOR = '#progress-dialog';
var CAPTCHA_FORM_SELECTOR = '#captcha-dialog';
var sectionText, inlineListing, replacements = {};
/**
* Return false if the current page should not enable the listing editor.
* Examples where the listing editor should not be enabled include talk
* pages, edit pages, history pages, etc.
*/
var listingEditorAllowedForCurrentPage = function() {
var namespace = mw.config.get( 'wgNamespaceNumber' );
if (namespace !== 0 && namespace !== 2 && namespace !== 4) {
return false;
}
if ( mw.config.get('wgAction') != 'view' || $('#mw-revision-info').length
|| mw.config.get('wgCurRevisionId') != mw.config.get('wgRevisionId')
|| $('#ca-viewsource').length ) {
return false;
}
return true;
};
/**
* Generate the form UI for the listing editor. If editing an existing
* listing, pre-populate the form input fields with the existing values.
*/
var createForm = function(mode, listingParameters, listingTemplateAsMap) {
var form = $(ListingEditor.Config.EDITOR_FORM_HTML);
// make sure the select dropdown includes any custom "type" values
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));
});
};
/**
* Utility function for appending the "add listing" link text to a heading.
*/
var insertAddListingPlaceholder = function(parentHeading) {
var editSection = $(parentHeading).next('.mw-editsection');
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>');
};
/**
* 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));
});
// if there is already metadata present add a separator
$(ListingEditor.Config.EDIT_LINK_CONTAINER_SELECTOR).each(function() {
if (!isElementEmpty(this)) {
$(this).append(' | ');
}
});
// append the edit link
$(ListingEditor.Config.EDIT_LINK_CONTAINER_SELECTOR).append( editButton );
};
/**
* Determine whether a listing entry is within a paragraph rather than
* an entry in a list; inline listings will be formatted slightly
* differently than entries in lists (no newlines in the template syntax,
* skip empty fields).
*/
var isInline = function(entry) {
// 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);
};
/**
* 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;
}
var link = heading.find('.mw-editsection a').attr('href');
return (link !== undefined) ? link.split('=').pop() : 0;
};
/**
* Given an edit link that was clicked for a listing, determine what index
* that listing is within a section. First listing is 0, second is 1, etc.
*/
var findListingIndex = function(sectionHeading, clicked) {
var count = 0;
$(EDIT_LINK_SELECTOR, sectionHeading).each(function() {
if (clicked.is($(this))) {
return false;
}
count++;
});
return count;
};
/**
* Return the listing template type appropriate for the section that
* contains the provided DOM element (example: "see" for "See" sections,
* etc). If no matching type is found then the default listing template
* type is returned.
*/
var findListingTypeForSection = function(entry) {
var sectionType = entry.closest('div.mw-h2section').children('h2').find('.mw-headline').attr('id');
for (var sectionId in ListingEditor.Config.SECTION_TO_TEMPLATE_TYPE) {
if (sectionType == sectionId) {
return ListingEditor.Config.SECTION_TO_TEMPLATE_TYPE[sectionId];
}
}
return ListingEditor.Config.DEFAULT_LISTING_TEMPLATE;
};
var replaceSpecial = function(str) {
return str.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&");
};
/**
* 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);
};
/**
* 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"
listingTemplateWikiSyntax = listingTemplateWikiSyntax.replace(typeRegex,'{{listing| ' + ListingEditor.Config.LISTING_TYPE_PARAMETER + '=$2$3');
// remove the trailing braces
listingTemplateWikiSyntax = listingTemplateWikiSyntax.slice(0,-2);
var listingTemplateAsMap = {};
var lastKey;
var listParams = listingTemplateToParamsArray(listingTemplateWikiSyntax);
for (var j=1; j < listParams.length; j++) {
var param = listParams[j];
var index = param.indexOf('=');
if (index > 0) {
// param is of the form key=value
var key = $.trim(param.substr(0, index));
var value = $.trim(param.substr(index+1));
listingTemplateAsMap[key] = value;
lastKey = key;
} else if (listingTemplateAsMap[lastKey].length) {
// there was a pipe character within a param value, such as
// "key=value1|value2", so just append to the previous param
listingTemplateAsMap[lastKey] += '|' + param;
}
}
for (var key in listingTemplateAsMap) {
// if the template value contains an HTML comment that was
// previously converted to a placehold then it needs to be
// converted back to a comment so that the placeholder is not
// displayed in the edit form
listingTemplateAsMap[key] = restoreComments(listingTemplateAsMap[key], false);
}
if (listingTemplateAsMap[ListingEditor.Config.LISTING_CONTENT_PARAMETER]) {
// convert paragraph tags to newlines so that the content is more
// readable in the editor window
listingTemplateAsMap[ListingEditor.Config.LISTING_CONTENT_PARAMETER] = listingTemplateAsMap[ListingEditor.Config.LISTING_CONTENT_PARAMETER].replace(/\s*<p>\s*/g, '\n\n');
listingTemplateAsMap[ListingEditor.Config.LISTING_CONTENT_PARAMETER] = listingTemplateAsMap[ListingEditor.Config.LISTING_CONTENT_PARAMETER].replace(/\s*<br\s*\/?>\s*/g, '\n');
}
// sanitize the listing type param to match the configured values, so
// if the listing contained "Do" it will still match the configured "do"
for (var key in ListingEditor.Config.LISTING_TEMPLATES) {
if (listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER].toLowerCase() === key.toLowerCase()) {
listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER] = key;
break;
}
}
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);
inlineListing = (mode === MODE_EDIT && isInline(clicked));
$.ajax({
url: mw.util.wikiScript(''),
data: { title: mw.config.get('wgPageName'), action: 'raw', section: sectionIndex },
cache: false // required
}).done(function(data, textStatus, jqXHR) {
sectionText = data;
openListingEditorDialog(mode, sectionIndex, listingIndex, listingType);
}).fail(function(jqXHR, textStatus, errorThrown) {
alert(ListingEditor.Config.TRANSLATIONS.ajaxInitFailure + ': ' + textStatus + ' ' + errorThrown);
});
};
/**
* This method is called asynchronously after the initListingEditorDialog()
* method has retrieved the existing wiki section content that the
* listing is being added to (and that contains the listing wiki syntax
* when editing).
*/
var openListingEditorDialog = function(mode, sectionNumber, listingIndex, listingType) {
sectionText = stripComments(sectionText);
mw.loader.using( ['jquery.ui'], function () {
var listingTemplateAsMap, listingTemplateWikiSyntax;
if (mode == MODE_ADD) {
listingTemplateAsMap = {};
listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER] = listingType;
} else {
listingTemplateWikiSyntax = getListingWikitextBraces(listingIndex);
listingTemplateAsMap = wikiTextToListing(listingTemplateWikiSyntax);
listingType = listingTemplateAsMap[ListingEditor.Config.LISTING_TYPE_PARAMETER];
}
var listingParameters = getListingInfo(listingType);
// if a listing editor dialog is already open, get rid of it
if ($(ListingEditor.Config.EDITOR_FORM_SELECTOR).length > 0) {
$(ListingEditor.Config.EDITOR_FORM_SELECTOR).dialog('destroy').remove();
}
var form = $(createForm(mode, listingParameters, listingTemplateAsMap));
// wide dialogs on huge screens look terrible
var windowWidth = $(window).width();
var dialogWidth = (windowWidth > ListingEditor.Config.MAX_DIALOG_WIDTH) ? ListingEditor.Config.MAX_DIALOG_WIDTH : 'auto';
// modal form - must submit or cancel
form.dialog({
modal: true,
height: 'auto',
width: dialogWidth,
title: (mode == MODE_ADD) ? ListingEditor.Config.TRANSLATIONS.addTitle : ListingEditor.Config.TRANSLATIONS.editTitle,
dialogClass: 'listing-editor-dialog',
buttons: [
{
text: '?',
id: 'listing-help',
click: function() { window.open(ListingEditor.Config.TRANSLATIONS.helpPage);}
},
{
text: ListingEditor.Config.TRANSLATIONS.submit, click: function() {
if (validateForm()) {
formToText(mode, listingTemplateWikiSyntax, listingTemplateAsMap, sectionNumber);
$(this).dialog('close');
}
}
},
{
text: ListingEditor.Config.TRANSLATIONS.preview,
title: ListingEditor.Config.TRANSLATIONS.preview,
id: 'listing-preview-button',
click: function() {
formToPreview(listingTemplateAsMap);
}
},
{
text: ListingEditor.Config.TRANSLATIONS.previewOff,
title: ListingEditor.Config.TRANSLATIONS.previewOff,
id: 'listing-previewOff',
style: 'display: none',
click: function() {
hidePreview();
}
},
{
text: ListingEditor.Config.TRANSLATIONS.refresh,
title: ListingEditor.Config.TRANSLATIONS.refreshTitle,
icon: 'ui-icon-refresh',
id: 'listing-refresh',
style: 'display: none',
click: function() {
refreshPreview(listingTemplateAsMap);
}
},
{
text: ListingEditor.Config.TRANSLATIONS.cancel,
click: function() {
$(this).dialog('destroy').remove();
}
}
],
create: function() {
$('.ui-dialog-buttonpane').append('<div class="listing-license">' + ListingEditor.Config.TRANSLATIONS.licenseText + '</div>');
}
});
});
};
/**
* 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(type) {
return (isCustomListingType(type)) ? ListingEditor.Config.LISTING_TEMPLATES[ListingEditor.Config.DEFAULT_LISTING_TEMPLATE] : ListingEditor.Config.LISTING_TEMPLATES[type];
};
/**
* Determine if the specified listing type is a custom type - for example "go"
* instead of "see", "do", "listing", etc.
*/
var isCustomListingType = function(listingType) {
return !(listingType in ListingEditor.Config.LISTING_TEMPLATES);
};
/**
* 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 <br> tags
$('#input-content').val($.trim($('#input-content').val()).replace(/\n+/g, '<br /><br />'));
// remove trailing period from price and address block
$('#input-price').val($.trim($('#input-price').val()).replace(/\.$/, ''));
$('#input-address').val($.trim($('#input-address').val()).replace(/\.$/, ''));
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 defaultListingParameters = getListingInfo(ListingEditor.Config.DEFAULT_LISTING_TEMPLATE);
var listingTypeInput = defaultListingParameters[ListingEditor.Config.LISTING_TYPE_PARAMETER].id;
var listingType = $("#" + listingTypeInput).val();
var listingParameters = getListingInfo(listingType);
for (var parameter in listingParameters) {
listing[parameter] = $("#" + listingParameters[parameter].id).val();
}
for (var i=0; i < ListingEditor.Callbacks.SUBMIT_FORM_CALLBACKS.length; i++) {
ListingEditor.Callbacks.SUBMIT_FORM_CALLBACKS[i](listing, mode);
}
var text = listingToStr(listing);
var summary = 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();
}
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 defaultListingParameters = getListingInfo(ListingEditor.Config.DEFAULT_LISTING_TEMPLATE);
var listingTypeInput = defaultListingParameters[ListingEditor.Config.LISTING_TYPE_PARAMETER].id;
var listingType = $("#" + listingTypeInput).val();
var listingParameters = getListingInfo(listingType);
for (var parameter in listingParameters) {
listing[parameter] = $("#" + listingParameters[parameter].id).val();
}
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
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.escapeIdForLink(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) {
$(SAVE_FORM_SELECTOR).dialog('destroy').remove();
captchaDialog(summary, minor, sectionNumber, data.edit.captcha.url, data.edit.captcha.id);
} else {
saveFailed(ListingEditor.Config.TRANSLATIONS.submitUnknownError);
}
}).fail(function(code, result) {
if (code === "http") {
saveFailed(ListingEditor.Config.TRANSLATIONS.submitHttpError + ': ' + result.textStatus );
} else if (code === "ok-but-empty") {
saveFailed(ListingEditor.Config.TRANSLATIONS.submitEmptyError);
} else {
saveFailed(ListingEditor.Config.TRANSLATIONS.submitUnknownError + ': ' + code );
}
});
savingForm();
};
/**
* If an error occurs while saving the form, remove the "saving" dialog,
* restore the original listing editor form (with all user content), and
* display an alert with a failure message.
*/
var saveFailed = function(msg) {
$(SAVE_FORM_SELECTOR).dialog('destroy').remove();
$(ListingEditor.Config.EDITOR_FORM_SELECTOR).dialog('open');
alert(msg);
};
/**
* If the result of an attempt to save the listing editor content is a
* Captcha challenge then display a form to allow the user to respond to
* the challenge and resubmit.
*/
var captchaDialog = function(summary, minor, sectionNumber, captchaImgSrc, captchaId) {
// if a captcha dialog is already open, get rid of it
if ($(CAPTCHA_FORM_SELECTOR).length > 0) {
$(CAPTCHA_FORM_SELECTOR).dialog('destroy').remove();
}
var captcha = $('<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(listingType);
var saveStr = '{{';
// type parameter always specified explicitly
saveStr += ListingEditor.Config.DEFAULT_LISTING_TEMPLATE;
saveStr += ' | ' + ListingEditor.Config.LISTING_TYPE_PARAMETER + '=' + listingType;
if (!inlineListing && listingParameters[ListingEditor.Config.LISTING_TYPE_PARAMETER].newline) {
saveStr += '\n';
}
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;
}
if (listing[parameter] !== '' || (!listingParameters[parameter].skipIfEmpty && !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
};
}();
$(document).ready(function() {
ListingEditor.Core.init();
});
} ( mediaWiki, jQuery ) )
);
//</nowiki>