Участник:Kiaora/ListingEditor.js
Внешний вид
Замечание: Возможно, после публикации вам придётся очистить кэш своего браузера, чтобы увидеть изменения.
- Firefox / Safari: Удерживая клавишу Shift, нажмите на панели инструментов Обновить либо нажмите Ctrl+F5 или Ctrl+R (⌘+R на Mac)
- Google Chrome: Нажмите Ctrl+Shift+R (⌘+Shift+R на Mac)
- Edge: Удерживая Ctrl, нажмите Обновить либо нажмите Ctrl+F5
- Opera: Нажмите Ctrl+F5.
/******************************************************************
Listing Editor v2.1
Original author:
- torty3
Additional contributors:
- Andyrom75
- Wrh2
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
- Add support for mobile devices.
- Handle "=" in listing parameter values ("[[File:XYZ.jpg|link=ABC]])"
- 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_TEMPLATE_PARAMETERS
********************************************************************/
//<nowiki>
( function ( mw, $ ) {
'use strict';
/* ***********************************************************************
* CUSTOMIZATION INSTRUCTIONS:
*
* Different Wikivoyage language versions have different implementations of
* the listing template, so this module must be customized for each. The
* ListingEditor.Config and ListingEditor.Callbacks modules should be the
* ONLY code that requires customization - ListingEditor.Core should be
* shared across all language versions. If for some reason the Core module
* must be modified, ideally the module should be modified for all language
* versions so that the code can stay in sync.
* ***********************************************************************/
var ListingEditor = {};
// see http://toddmotto.com/mastering-the-module-pattern/ for an overview
// of the module design pattern being used in this gadget
/* ***********************************************************************
* ListingEditor.Config 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 = 'en';
var COMMONS_URL = '//commons.wikimedia.org';
var WIKIDATA_URL = '//www.wikidata.org';
var TRANSLATIONS = {
'addTitle' : 'Добавить объект',
'editTitle' : 'Редактировать объект',
'add': 'добавить объект',
'edit': 'редактировать',
'saving': 'Сохранение...',
'submit': 'Сохранить',
'cancel': 'Отмена',
'validationEmptyListing': 'Пожалуйста, укажите название или адрес',
'validationImage': 'Пожалуйста, введите имя файла без каких-либо префиксов',
'image': '', //Local prefix for Image (or File)
'added': 'Добавлен объект ',
'updated': 'Обновлён объект ',
'removed': 'Удалён объект ',
'helpPage': '//ru.wikivoyage.org/wiki/Обсуждение_MediaWiki:Gadget-ListingEditor.js',
'enterCaptcha': 'Введите CAPTCHA',
'externalLinks': 'Введённый текст содержит внешние ссылки.',
// license text should match MediaWiki:Wikimedia-copyrightwarning
'licenseText': 'Нажимая кнопку «Сохранить», вы соглашаетесь с <a class="external" target="_blank" href="https://wikimediafoundation.org/wiki/Terms_of_Use/ru">условиями использования</a>, а также соглашаетесь на неотзывную публикацию по лицензии <a class="external" target="_blank" href="https://en.wikipedia.org/wiki/ru:Википедия:Текст_Лицензии_Creative_Commons_Attribution-ShareAlike_3.0_Unported">CC-BY-SA 3.0</a>.',
'ajaxInitFailure': 'Ошибка: невозможно инициализировать листинг эдитор',
'sharedImage': 'Изображение',
'sharedLatitude': 'Широта',
'sharedLongitude': 'Долгота',
'sharedWebsite': 'Веб-сайт',
'submitApiError': 'Во время сохранения листинга на сервере произошла ошибка, пожайлуста, попробуйте сохранить ещё раз',
'submitBlacklistError': 'Ошибка: текст содержит ссылку из чёрного списка, пожайлуста, удалите её и попробуйте сохранить снова',
'submitUnknownError': 'Ошибка: при попытке сохранить листинг произошла неизвестная ошибка, пожайлуста, попробуйте сохранить ещё раз',
'submitHttpError': 'Ошибка: сервер сообщил о HTTP ошибке, возникшей во время сохранения листинга, пожайлуста, попробуйте сохранить ещё раз',
'submitEmptyError': 'Ошибка: сервер вернул пустой ответ при попытке сохранить листинг, пожайлуста, попробуйте сохранить ещё раз',
'viewCommonsPage' : 'открыть изображение на новой странице',
'viewWikidataPage' : 'посмотреть запись Викиданных',
'wikidataShared': 'Следующая информация доступна в Викиданных. Обновить эти данные в листинге?',
'wikidataSharedNotFound': 'На Викиданных информации, возможной к импортированию, не найдено.'
};
// --------------------------------------------------------------------
// 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_TEMPLATE_PARAMETERS parameter arrays (such as wikipedia, phoneextra, etc).
// if the flag is set to true then unrecognized parameters will be allowed
// as long as they have a non-empty value.
var ALLOW_UNRECOGNIZED_PARAMETERS = true;
// --------------------------------------------------------------------
// UPDATE THE FOLLOWING TO MATCH WIKIVOYAGE ARTICLE SECTION NAMES
// --------------------------------------------------------------------
// map section heading ID to the listing template to use for that section
var SECTION_TO_TEMPLATE_TYPE = {
'Как добраться': 'go',
'Достопримечательности': 'see',
'Чем заняться': 'do',
'Покупки': 'buy',
'Еда': 'eat',
'Ночная жизнь': 'drink',
'Где остановиться': 'sleep',
'Связь': 'other',
'Окрестности':'vicinity'
};
// 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 = ['#Города', '#Областные центры', '#Курорты', '#Острова', '#Малые города', '#Другие места назначения', '#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
//**RUS** commented out
//var DEFAULT_LISTING_TEMPLATE = 'listing';
//var LISTING_TYPE_PARAMETER = 'type';
//var LISTING_CONTENT_PARAMETER = 'description';
// selector that identifies the HTML elements into which the 'edit' link
// for each listing will be placed
//**RUS** changed 'span.listing-metadata-items' to '.mw-addhere'
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_TEMPLATE_PARAMETERS
// 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.
//**RUS** removed 'skipIfEmpty', SLEEP_TEMPLATE_PARAMETERS. Template parameters: "checkin"/"checkout", 'tollfree', 'fax', 'wikipedia', globally changed 'content'->'description'
var LISTING_TEMPLATE_PARAMETERS = {
'type': { id:'input-type' },
'lat': { id:'input-lat' },
'long': { id:'input-long' },
'wdid': { id:'input-wdid-value', newline: true },
'name': { id:'input-name' },
'alt': { id:'input-alt' },
'image': { id:'input-image', newline: true },
'address': { id:'input-address' },
'directions': { id:'input-directions', newline: true },
'url': { id:'input-url' },
'facebook': { id:'input-face-book' },
'vkontakte': { id:'input-vkontakte', newline: true },
'phone': { id:'input-phone', newline: true },
'email': { id:'input-email', newline: true },
'hours': { id:'input-hours' },
'price': { id:'input-price', newline: true },
'lastedit': { id:'input-lastedit', newline: true },
'description': { id:'input-description', newline: true },
'star': { id:'input-star', newline: true},
'unesco': { id:'input-unesco', newline: true },
'format': { id:'input-format', newline: true }
};
// --------------------------------------------------------------------
// 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_TEMPLATE_PARAMETERS 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-partialwidth-75" placeholder="название объекта" id="input-name"> ' +
'<a id="name-quotes-template" href="javascript:">«»</a> ' +
'<a id="name-dash-template" href="javascript:">—</a>' +
'</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_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-partialwidth-75" placeholder="+55 555 555-5555" id="input-phone"> ' +
'<a id="phone-template" href="javascript:">{{phone|||}}</a>' +
'</td>' +
'</tr>' +
'<tr id="div_hours">' +
'<td class="editor-label-col"><label for="input-hours">Часы работы</label></td>' +
'<td>' +
'<input type="text" class="editor-partialwidth-75" placeholder="09:00-18:00" id="input-hours"> ' +
'<a id="hours-template" href="javascript:">{{hours|||}}</a>' +
'</td>' +
'</tr>' +
'<tr id="div_price">' +
'<td class="editor-label-col"><label for="input-price">Стоимость</label></td>' +
'<td>' +
// update the ListingEditor.Callbacks.initCurrencySymbolFormFields
// method if the currency symbols are removed or modified
'<input type="text" class="editor-partialwidth-75" placeholder="100 руб" id="input-price"> ' +
'<span id="span_currency">' +
'<span class="currency-signs"> <a href="javascript:">\u20AC</a></span>' +
'</span>' +
'</td>' +
'</tr>' +
'<tr id="div_url">' +
'<td class="editor-label-col"><label for="input-url">Веб-сайт<span class="wdid-update"></span></label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="http://www.example.com" id="input-url"></td>' +
'</tr>' +
'<tr id="div_email" style="display: none;">' +
'<td class="editor-label-col"><label for="input-email">E-mail</label></td>' +
'<td><input type="text" class="editor-fullwidth" placeholder="hello@example.com" id="input-email"></td>' +
'</tr>' +
'<tr id="div_vkontakte">' +
'<td class="editor-label-col"><label for="input-vkontakte">ВКонтакте</label></td>' +
'<td><input type="text" class="editor-partialwidth-75" placeholder="http://vk.com/example" id="input-vkontakte"></td>' +
'</tr>' +
'<tr id="div-face-book">' +
'<td class="editor-label-col"><label for="input-face-book">Facebook</label></td>' +
'<td><input type="text" class="editor-partialwidth-75" placeholder="https://www.facebook.com/example" id="input-face-book"></td>' +
'</tr>' +
'<tr id="div_lastedit" style="display: none;">' +
'<td class="editor-label-col"><label for="input-lastedit">Last Updated</label></td>' +
'<td><input type="text" size="10" placeholder="2016-01-15" id="input-lastedit"></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">Тип</label></td>' +
'<td>' +
'<select id="input-type" placeholder="тип объекта, определяющий цвет значка на карте">' +
'<option value="go">Как добраться</option>' +
'<option value="see">Достопримечательности</option>' +
'<option value="do">Чем заняться</option>' +
'<option value="buy">Покупки</option>' +
'<option value="eat">Еда</option>' +
'<option value="drink">Ночная жизнь</option>' +
'<option value="sleep">Где остановиться</option>' +
'<option value="other">Связь</option>' +
'<option value="vicinity">Окрестности</option>' +
'</select>' +
'</td>' +
'</tr>' +
'<tr id="div_format" style="display: none;">' +
'<td class="editor-label-col"><label for="input-format">Форматирование</label></td>' +
'<td><input type="text" size="10" placeholder="" id="input-format"></td>' +
'</tr>' +
'<tr id="div_lat">' +
'<td class="editor-label-col"><label for="input-lat">Широта<span class="wdid-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="http://maps.wikivoyage-ev.org/w/geomap.php">Найти на карте</a></td>' +
'</tr>' +
'<tr id="div_long">' +
'<td class="editor-label-col"><label for="input-long">Долгота<span class="wdid-update"></span></label></td>' +
'<td><input type="text" class="editor-partialwidth" placeholder="111.11111" id="input-long"></td>' +
'</tr>' +
'<tr id="div_wdid">' +
'<td class="editor-label-col"><label for="input-wdid-label">Викиданные</label></td>' +
'<td>' +
'<input type="text" class="editor-partialwidth" placeholder="Qxxxxx" id="input-wdid-label">' +
'<input type="hidden" id="input-wdid-value">' +
'<span id="wdid-value-display-container" style="display:none">' +
'<small>' +
' <span id="wdid-value-link"></span>' +
' | <a href="javascript:" id="wdid-remove" title="Удалить запись Викиданных из этого листинга">удалить</a>' +
'</small>' +
'</span>' +
'</td>' +
'</tr>' +
'<tr id="div_wdid_update" style="display: none">' +
'<td class="editor-label-col"> </td>' +
'<td><span class="wdid-update"></span><a href="javascript:" id="wdid-shared">Импортировать информацию со страницы Викиданных</a></td>' +
'</tr>' +
'<tr id="div_image">' +
'<td class="editor-label-col"><label for="input-image">Изображение<span class="wdid-update"></span></label></td>' +
'<td>' +
'<input type="text" class="editor-partialwidth" placeholder="имя файла на Commons" 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">' +
// update the ListingEditor.Callbacks.hideEditOnlyFields method if
// the status row is removed or modified
'<tr id="div_status">' +
'<td class="editor-label-col"><label>Статус</label></td>' +
'<td>' +
// 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" checked="checked"/>' +
'<label for="input-last-edit" class="listing-tooltip" title="Установите галочку, если вся информация об объекте достоверна и актуальна, и дате последнего обновления будет присвоено сегодняшнее число">Информация актуальна на сегодняшнюю дату?</label>' +
'</span>' +
'<span id="span-star">' +
'<input type="checkbox" id="input-star">' +
'<label for="input-star" class="listing-tooltip" title="Установите галочку, если объект является ключевым при посещении данного населённого пункта">Важный?</label>' +
'</span>' +
'<span id="span-unesco">' +
'<input type="checkbox" id="input-unesco">' +
'<label for="input-unesco" class="listing-tooltip" title="Установите галочку, если данный объект включён в список всемирного наследия ЮНЕСКО">ЮНЕСКО?</label>' +
'</span>' +
'<span id="span-closed">' +
'<input type="checkbox" id="input-closed">' +
'<label for="input-closed" class="listing-tooltip" title="Установите галочку, если данный объект больше не функционирует, и он будет удалён из статьи">Удалить этот объект?</label>' +
'</span>' +
'</td>' +
'</tr>' +
'<tr id="div_description">' +
'<td class="editor-label-col"><label for="input-description">Описание</label></td>' +
'<td><textarea rows="8" class="editor-fullwidth" placeholder="описание объекта" id="input-description"></textarea></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">Описание изменений</label></td>' +
'<td>' +
'<input type="text" class="editor-partialwidth" placeholder="что именно было изменено" id="input-summary">' +
'<span id="span-minor"><input type="checkbox" id="input-minor"><label for="input-minor" class="listing-tooltip" title="Установите галочку, если изменение незначительное, например, исправление опечатки">незначительное изменение?</label></span>' +
'</td>' +
'</tr>' +
'</table>' +
'</form>';
// expose public members
return {
LANG: LANG,
COMMONS_URL: COMMONS_URL,
WIKIDATA_URL: WIKIDATA_URL,
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,
SECTION_TO_TEMPLATE_TYPE: SECTION_TO_TEMPLATE_TYPE,
LISTING_TEMPLATE_PARAMETERS: LISTING_TEMPLATE_PARAMETERS,
EDITOR_FORM_SELECTOR: EDITOR_FORM_SELECTOR,
EDITOR_CLOSED_SELECTOR: EDITOR_CLOSED_SELECTOR,
EDITOR_SUMMARY_SELECTOR: EDITOR_SUMMARY_SELECTOR,
EDITOR_MINOR_EDIT_SELECTOR: EDITOR_MINOR_EDIT_SELECTOR,
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
// --------------------------------------------------------------------
//**RUS** added the next method
var initQuotesInsert = function(form, mode) {
$('#name-quotes-template', form).click(function() {
var input = $('#input-name');
var selectionStart = input[0].selectionStart;
var selectionEnd = input[0].selectionEnd;
var oldValue = input.val();
var newValue = oldValue.substring(0, selectionStart) + "«" + oldValue.substring(selectionStart, selectionEnd) + "»" + oldValue.substring(selectionEnd);
input.val(newValue);
});
};
CREATE_FORM_CALLBACKS.push(initQuotesInsert);
//**RUS** added the next method
var initQuotesInsert = function(form, mode) {
$('#name-dash-template', form).click(function() {
var input = $('#input-name');
var caretPos = input[0].selectionStart;
var oldValue = input.val();
var newValue = oldValue.substring(0, caretPos) + "—" + oldValue.substring(caretPos);
input.val(newValue);
});
};
CREATE_FORM_CALLBACKS.push(initQuotesInsert);
/**
* 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);
//**RUS** added the next method
var initPhoneTemplateInsert = function(form, mode) {
$('#phone-template', form).click(function() {
var input = $('#input-phone');
var caretPos = input[0].selectionStart;
var oldValue = input.val();
var newValue = oldValue.substring(0, caretPos) + "{{phone|||}}" + oldValue.substring(caretPos);
input.val(newValue);
});
};
CREATE_FORM_CALLBACKS.push(initPhoneTemplateInsert);
//**RUS** added the next method
var initHoursTemplateInsert = function(form, mode) {
$('#hours-template', form).click(function() {
var input = $('#input-hours');
var caretPos = input[0].selectionStart;
var oldValue = input.val();
var newValue = oldValue.substring(0, caretPos) + "{{hours|день1|день2|час1|мин1|час2|мин2}}" + oldValue.substring(caretPos);
input.val(newValue);
});
};
CREATE_FORM_CALLBACKS.push(initHoursTemplateInsert);
/**
* 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) {
//**RUS** modified 1 line
var EDITOR_STATUS_ROW = '#span-last-edit,#span-closed';
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 wikidataLookup = function(form, mode) {
// get the display value for the pre-existing wikidata record ID
var value = $("#input-wdid-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-wdid-value").val();
var label = ListingEditor.SisterSite.wikidataLabel(jsonObj, value);
if (label === null) {
label = "";
}
$("#input-wdid-label").val(label);
};
ListingEditor.SisterSite.ajaxSisterSiteSearch(ajaxUrl, ajaxData, ajaxSuccess);
}
// set up autocomplete to search for results as the user types
$('#input-wdid-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-wdid-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
$('#wdid-remove', form).click(function() {
wikidataRemove(form);
});
$('#input-wdid-label', form).change(function() {
if (!$(this).val()) {
wikidataRemove(form);
}
});
var wikidataRemove = function(form) {
$("#input-wdid-value", form).val("");
$("#input-wdid-label", form).val("");
$("#wdid-value-display-container", form).hide();
$('#div_wdid_update', form).hide();
};
$('#wdid-shared', form).click(function() {
var wikidataRecord = $("#input-wdid-value", form).val();
updateWikidataSharedFields(wikidataRecord);
});
var commonsSiteData = {
apiUrl: ListingEditor.SisterSite.API_COMMONS,
selector: $('#input-image', form),
form: form,
ajaxData: {
namespace: 6
},
updateLinkFunction: commonsLink
};
ListingEditor.SisterSite.initializeSisterSiteAutocomplete(commonsSiteData);
};
var commonsLink = function(value, form) {
var commonsSiteLinkData = {
inputSelector: '#input-image',
containerSelector: '#image-value-display-container',
linkContainerSelector: '#image-value-link',
href: ListingEditor.Config.COMMONS_URL + '/wiki/' + mw.util.wikiUrlencode('File:' + value),
linkTitle: ListingEditor.Config.TRANSLATIONS.viewCommonsPage
};
sisterSiteLinkDisplay(commonsSiteLinkData, form);
};
var sisterSiteLinkDisplay = function(siteLinkData, form) {
var value = $(siteLinkData.inputSelector, form).val();
if (!value) {
$(siteLinkData.containerSelector, form).hide();
} else {
var link = $("<a />", {
target: "_new",
href: siteLinkData.href,
title: siteLinkData.linkTitle,
text: siteLinkData.linkTitle
});
$(siteLinkData.linkContainerSelector, form).html(link);
$(siteLinkData.containerSelector, form).show();
}
};
var updateFieldIfNotNull = function(selector, value) {
if (value) {
$(selector).val(value);
}
};
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 coords = ListingEditor.SisterSite.wikidataClaim(jsonObj, wikidataRecord, ListingEditor.SisterSite.WIKIDATA_CLAIM_COORD);
var link = ListingEditor.SisterSite.wikidataClaim(jsonObj, wikidataRecord, ListingEditor.SisterSite.WIKIDATA_CLAIM_LINK);
var image = ListingEditor.SisterSite.wikidataClaim(jsonObj, wikidataRecord, ListingEditor.SisterSite.WIKIDATA_CLAIM_IMAGE);
var msg = '';
if (coords) {
// trim lat/long to six decimal places
coords.latitude = ListingEditor.Core.trimDecimal(coords.latitude, 6);
coords.longitude = ListingEditor.Core.trimDecimal(coords.longitude, 6);
msg += '\n' + ListingEditor.Config.TRANSLATIONS.sharedLatitude + ': ' + coords.latitude;
msg += '\n' + ListingEditor.Config.TRANSLATIONS.sharedLongitude + ': ' + coords.longitude;
}
if (link) {
msg += '\n' + ListingEditor.Config.TRANSLATIONS.sharedWebsite + ': ' + link;
}
if (image) {
msg += '\n' + ListingEditor.Config.TRANSLATIONS.sharedImage + ': ' + image;
}
if (msg) {
if (confirm(ListingEditor.Config.TRANSLATIONS.wikidataShared + '\n' + msg)) {
if (coords) {
updateFieldIfNotNull('#input-lat', coords.latitude);
updateFieldIfNotNull('#input-long', coords.longitude);
}
updateFieldIfNotNull('#input-url', link);
updateFieldIfNotNull('#input-image', image);
if (image) {
commonsLink(image);
}
}
} else {
alert(ListingEditor.Config.TRANSLATIONS.wikidataSharedNotFound);
}
};
ListingEditor.SisterSite.ajaxSisterSiteSearch(ajaxUrl, ajaxData, ajaxSuccess);
};
var parseWikiDataResult = function(jsonObj) {
var results = [];
for (var i=0; i < $(jsonObj.search).length; i++) {
var result = $(jsonObj.search)[i];
var label = result.label;
if (result.match && result.match.text) {
label = result.match.text;
}
var data = {
value: label,
label: label,
description: result.description,
id: result.id
};
results.push(data);
}
return results;
};
var wikidataLink = function(form, value) {
var link = $("<a />", {
target: "_new",
href: ListingEditor.Config.WIKIDATA_URL + '/wiki/' + mw.util.wikiUrlencode(value),
title: ListingEditor.Config.TRANSLATIONS.viewWikidataPage,
text: value
});
$("#wdid-value-link", form).html(link);
$("#wdid-value-display-container", form).show();
$('#div_wdid_update', form).show();
};
CREATE_FORM_CALLBACKS.push(wikidataLookup);
// --------------------------------------------------------------------
// LISTING EDITOR FORM SUBMISSION CALLBACKS
// --------------------------------------------------------------------
/**
* Return the current date in the format "2015-01-15".
*/
var currentLastEditDate = function() {
var d = new Date();
var year = d.getFullYear();
// Date.getMonth() returns 0-11
var month = d.getMonth() + 1;
if (month < 10) month = '0' + month;
var day = d.getDate();
if (day < 10) day = '0' + day;
return year + '-' + month + '-' + day;
};
/**
* Only update last edit date if this is a new listing or if the
* "information up-to-date" box checked.
*/
var updateLastEditDate = function(listing, mode) {
var LISTING_LAST_EDIT_PARAMETER = 'lastedit';
var EDITOR_LAST_EDIT_SELECTOR = '#input-last-edit';
if (mode == ListingEditor.Core.MODE_ADD || $(EDITOR_LAST_EDIT_SELECTOR).is(':checked')) {
listing[LISTING_LAST_EDIT_PARAMETER] = currentLastEditDate();
}
};
SUBMIT_FORM_CALLBACKS.push(updateLastEditDate);
// --------------------------------------------------------------------
// LISTING EDITOR FORM VALIDATION CALLBACKS
// --------------------------------------------------------------------
/**
* Verify all listings have at least a name, address or alt value.
*/
var validateListingHasData = function(validationFailureMessages) {
if ($('#input-name').val() === '' && $('#input-address').val() === '' && $('#input-alt').val() === '') {
validationFailureMessages.push(ListingEditor.Config.TRANSLATIONS.validationEmptyListing);
}
};
VALIDATE_FORM_CALLBACKS.push(validateListingHasData);
/**
* 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_COMMONS = ListingEditor.Config.COMMONS_URL + '/w/api.php';
var WIKIDATA_CLAIM_COORD = 'P625';
var WIKIDATA_CLAIM_LINK = 'P856';
var WIKIDATA_CLAIM_IMAGE = 'P18';
var _initializeSisterSiteAutocomplete = function(siteData) {
var currentValue = $(siteData.selector).val();
if (currentValue) {
siteData.updateLinkFunction(currentValue, siteData.form);
}
$(siteData.selector).change(function() {
siteData.updateLinkFunction($(siteData.selector).val(), siteData.form);
});
siteData.selectFunction = function(event, ui) {
siteData.updateLinkFunction(ui.item.value, siteData.form);
};
var ajaxData = siteData.ajaxData;
ajaxData.action = 'opensearch';
ajaxData.list = 'search';
ajaxData.limit = 10;
ajaxData.redirects = 'resolve';
var parseAjaxResponse = function(jsonObj) {
var results = [];
var titleResults = $(jsonObj[1]);
for (var i=0; i < titleResults.length; i++) {
var result = titleResults[i];
var valueWithoutFileNamespace = (titleResults[i].indexOf("File:") != -1) ? titleResults[i].substring("File:".length) : titleResults[i];
var titleResult = { value: valueWithoutFileNamespace, label: titleResults[i], description: $(jsonObj[2])[i], link: $(jsonObj[3])[i] };
results.push(titleResult);
}
return results;
};
_initializeAutocomplete(siteData, ajaxData, parseAjaxResponse);
};
var _initializeAutocomplete = function(siteData, ajaxData, parseAjaxResponse) {
var autocompleteOptions = {
source: function(request, response) {
ajaxData.search = request.term;
var ajaxSuccess = function(jsonObj) {
response(parseAjaxResponse(jsonObj));
};
_ajaxSisterSiteSearch(siteData.apiUrl, ajaxData, ajaxSuccess);
}
};
if (siteData.selectFunction) {
autocompleteOptions.select = siteData.selectFunction;
}
siteData.selector.autocomplete(autocompleteOptions);
};
// perform an ajax query of a sister site
var _ajaxSisterSiteSearch = function(ajaxUrl, ajaxData, ajaxSuccess) {
ajaxData.format = 'json';
$.ajax({
url: ajaxUrl,
data: ajaxData,
dataType: 'jsonp',
success: ajaxSuccess
});
};
// parse the wikidata "claim" object from the wikidata response
var _wikidataClaim = function(jsonObj, value, property) {
var entity = _wikidataEntity(jsonObj, value);
if (!entity || !entity.claims || !entity.claims[property]) {
return null;
}
var propertyObj = entity.claims[property];
if (!propertyObj || propertyObj.length < 1 || !propertyObj[0].mainsnak || !propertyObj[0].mainsnak.datavalue) {
return null;
}
return propertyObj[0].mainsnak.datavalue.value;
};
// parse the wikidata "entity" object from the wikidata response
var _wikidataEntity = function(jsonObj, value) {
if (!jsonObj || !jsonObj.entities || !jsonObj.entities[value]) {
return null;
}
return jsonObj.entities[value];
};
// parse the wikidata display label from the wikidata response
var _wikidataLabel = function(jsonObj, value) {
var entityObj = _wikidataEntity(jsonObj, value);
if (!entityObj || !entityObj.labels || !entityObj.labels.en) {
return null;
}
return entityObj.labels.en.value;
};
// expose public members
return {
API_WIKIDATA: API_WIKIDATA,
API_COMMONS: API_COMMONS,
WIKIDATA_CLAIM_COORD: WIKIDATA_CLAIM_COORD,
WIKIDATA_CLAIM_LINK: WIKIDATA_CLAIM_LINK,
WIKIDATA_CLAIM_IMAGE: WIKIDATA_CLAIM_IMAGE,
initializeSisterSiteAutocomplete: _initializeSisterSiteAutocomplete,
ajaxSisterSiteSearch: _ajaxSisterSiteSearch,
wikidataClaim: _wikidataClaim,
wikidataLabel: _wikidataLabel
};
}();
/* ***********************************************************************
* 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, listingTemplateAsMap) {
var form = $(ListingEditor.Config.EDITOR_FORM_HTML);
// make sure the select dropdown includes any custom "type" values
//**RUS** commented out
//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>');
//}
//**RUS** modified
var listingParameters = ListingEditor.Config.LISTING_TEMPLATE_PARAMETERS;
// populate the empty form with existing values
for (var parameter in ListingEditor.Config.LISTING_TEMPLATE_PARAMETERS) {
var paramId = listingParameters[parameter].id;
var input = $('#' + paramId, form);
var paramValue = listingTemplateAsMap[parameter];
if (paramValue) {
if (parameter == "star" || parameter == "unesco") {
if (paramValue == "yes") {
input.prop('checked', true);
}
} else {
input.val(paramValue);
}
if (parameter == "format") {
$('#div_format', form).show();
} if (parameter == "email") {
$('#div_email', form).show();
}
}
}
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) {
sectionId = encodeURIComponent(sectionId).replace(/%20/g,'_').replace(/%/g,'.');
// use a pattern rather than searching by ID directly in case
// the article uses top-level headings in sub-sections
//**RUS** changed # to [id=
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">')
//**RUS** added class and title parameters
.html('<a href="javascript:" class="icon-pencil" title="' + ListingEditor.Config.TRANSLATIONS.editTitle + '">'+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)) {
//**RUS** removed '|' sign
$(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');
//**RUS** added the following line
sectionType = decodeURIComponent(sectionType.replace(/\./g,'%').replace(/_/g,'%20'));
for (var sectionId in ListingEditor.Config.SECTION_TO_TEMPLATE_TYPE) {
if (sectionType == sectionId) {
return ListingEditor.Config.SECTION_TO_TEMPLATE_TYPE[sectionId];
}
}
//**RUS** changed 'listing' to 'other'
return "other";
};
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_TEMPLATE_PARAMETERS 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_TEMPLATE_PARAMETERS) {
regex.push(key);
}
//**RUS** modified regex.join('|') to listing
return new RegExp('{{\\s*(listing)\\b','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[0];
}
// 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();
// remove the trailing braces
listingTemplateWikiSyntax = listingTemplateWikiSyntax.slice(0,-2);
// convert "{{see" to {{listing|type=see"
listingTemplateWikiSyntax = listingTemplateWikiSyntax.replace(typeRegex,'{{listing|type=$1');
var listingTemplateAsMap = {};
var lastKey;
var listParams = listingTemplateToParamsArray(listingTemplateWikiSyntax);
for (var j=1; j < listParams.length; j++) {
var param = listParams[j];
var index = param.indexOf('=');
if (index > 0) {
// param is of the form key=value
var key = $.trim(param.substr(0, index));
var value = $.trim(param.substr(index+1));
listingTemplateAsMap[key] = value;
lastKey = key;
} else if (listingTemplateAsMap[lastKey].length) {
// there was a pipe character within a param value, such as
// "key=value1|value2", so just append to the previous param
listingTemplateAsMap[lastKey] += '|' + param;
}
}
for (var key in listingTemplateAsMap) {
// if the template value contains an HTML comment that was
// previously converted to a placehold then it needs to be
// converted back to a comment so that the placeholder is not
// displayed in the edit form
listingTemplateAsMap[key] = restoreComments(listingTemplateAsMap[key], false);
}
if (listingTemplateAsMap["description"]) {
// convert paragraph tags to newlines so that the description is more
// readable in the editor window
listingTemplateAsMap["description"] = listingTemplateAsMap["description"].replace(/\s*<p>\s*/g, '\n\n');
}
// sanitize the listing type param to match the configured values, so
// if the listing contained "Do" it will still match the configured "do"
for (var key in ListingEditor.Config.LISTING_TEMPLATE_PARAMETERS) {
if (listingTemplateAsMap["type"].toLowerCase() === key.toLowerCase()) {
listingTemplateAsMap["type"] = 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
* description 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 description 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["type"] = listingType;
} else {
listingTemplateWikiSyntax = getListingWikitextBraces(listingIndex);
listingTemplateAsMap = wikiTextToListing(listingTemplateWikiSyntax);
listingType = listingTemplateAsMap["type"];
}
// 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, listingTemplateAsMap));
// wide dialogs on huge screens look terrible
var windowWidth = $(window).width();
var dialogWidth = (windowWidth > ListingEditor.Config.MAX_DIALOG_WIDTH) ? ListingEditor.Config.MAX_DIALOG_WIDTH : 'auto';
// modal form - must submit or cancel
form.dialog({
modal: true,
height: 'auto',
width: dialogWidth,
title: (mode == MODE_ADD) ? ListingEditor.Config.TRANSLATIONS.addTitle : ListingEditor.Config.TRANSLATIONS.editTitle,
dialogClass: 'listing-editor-dialog',
buttons: [
{
text: '?',
id: 'listing-help',
click: function() { window.open(ListingEditor.Config.TRANSLATIONS.helpPage);}
},
{
text: ListingEditor.Config.TRANSLATIONS.submit, click: function() {
if (validateForm()) {
formToText(mode, listingTemplateWikiSyntax, listingTemplateAsMap, sectionNumber);
$(this).dialog('close');
}
}
},
{
text: ListingEditor.Config.TRANSLATIONS.cancel,
click: function() {
$(this).dialog('destroy').remove();
}
}
],
create: function() {
$('.ui-dialog-buttonpane').append('<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;
};
/**
* Determine if the specified listing type is a custom type - for example "go"
* instead of "see", "do", "listing", etc.
*/
//**RUS** no need - commented out
//var isCustomListingType = function(listingType) {
//return !(listingType in ListingEditor.Config.LISTING_TEMPLATE_PARAMETERS);
//};
/**
* 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 description won't render properly in lists, so
// replace them with <p> tags
$('#input-description').val($.trim($('#input-description').val()).replace(/\n+/g, '<p>'));
var webRegex = new RegExp('^https?://', 'i');
var url = $('#input-url').val();
if (!webRegex.test(url) && url !== '') {
$('#input-url').val('http://' + url);
}
return true;
};
/**
* Convert the listing editor form entry fields into wiki text. This
* method converts the form entry fields into a listing template string,
* replaces the original template string in the section text with the
* updated entry, and then submits the section text to be saved on the
* server.
*/
var formToText = function(mode, listingTemplateWikiSyntax, listing, sectionNumber) {
for (var parameter in ListingEditor.Config.LISTING_TEMPLATE_PARAMETERS) {
var input = $("#" + ListingEditor.Config.LISTING_TEMPLATE_PARAMETERS[parameter].id);
//**RUS** added the following condition
if (parameter == 'star' || parameter == 'unesco') {
listing[parameter]= input.is(':checked') ? 'yes' : '';
} else {
listing[parameter] = input.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;
};
/**
* 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('====');
}
//**RUS** add condition to make sure listingWikiText gets inserted before the footer
if (index === 0) {
index = sectionText.indexOf('{{footer');
}
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 = encodeURIComponent(getSectionName()).replace(/%20/g,'_').replace(/%/g,'.');
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 description), 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 description 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["type"];
var listingParameters = ListingEditor.Config.LISTING_TEMPLATE_PARAMETERS;
var saveStr = '{{';
//**RUS** modified
saveStr += 'listing|type=' + listingType;
if (!inlineListing && listingParameters["type"].newline) {
saveStr += '\n';
}
for (var parameter in listingParameters) {
if (parameter === "type") {
// "type" parameter was handled previously
continue;
}
if (parameter === "description") {
// processed last
continue;
}
// **RUS** if format, unesco or star parameter is empty - don't include it in the listing
if (((parameter == 'format') || (parameter == 'unesco') || (parameter == 'star')) && listing[parameter] == '') {
continue;
}
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 += '| description=' + listing["description"];
saveStr += (inlineListing || !listingParameters["description"].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>