MediaWiki:Gadget-wstaw-link-interwiki.js
Wygląd
Uwaga: aby zobaczyć zmiany po opublikowaniu, może zajść potrzeba wyczyszczenia pamięci podręcznej przeglądarki.
- Firefox / Safari: Przytrzymaj Shift podczas klikania Odśwież bieżącą stronę, lub naciśnij klawisze Ctrl+F5, lub Ctrl+R (⌘-R na komputerze Mac)
- Google Chrome: Naciśnij Ctrl-Shift-R (⌘-Shift-R na komputerze Mac)
- Edge: Przytrzymaj Ctrl, jednocześnie klikając Odśwież, lub naciśnij klawisze Ctrl+F5.
- Opera: Naciśnij klawisze Ctrl+F5.
//@ts-check
/**
* @author [[w:pl:User:Msz2001]]
*
* Skrypt ułatwia zamianę czerwonych linków na szablon {{link-interwiki}}.
*
* <nowiki>
*/
$(function(){
var LINK_TARGET_INDEX = 'data-target-index';
var LINK_FROM_TEMPLATE = 'data-from-template';
var STORAGE_DIFF_KEY = 'gadget-link-interwiki-diff';
var TRIGGER_TEXT = '{{link-interwiki}}';
var TRIGGER_TOOLTIP = 'Zamień czerwone linki na szablon {{link-interwiki}}';
var SHOW_DIFF_CHECKBOX = 'Podgląd zmian';
var SAVE_BUTTON = 'Zapisz';
var DISCARD_BUTTON = 'Odrzuć';
var SAVING_TEXT = 'Zapisywanie...';
var COUNTER_TEXT = 'Zamieniono linków: $1 / $2';
var LINK_TITLE = 'Zamień na {{link-interwiki}} powiązany z Wikidanymi';
var QID_POPUP_TITLE = 'Wiązanie elementu Wikidanych';
var TEMPLATE_IN_LINK = 'Uwaga: Link zawiera w sobie szablon lub inne znaczniki wikikodu. Jeśli kontynuujesz, zostaną one usunięte z tego linku.';
var PROMPT_TEXT = 'Podaj identyfikator elementu w Wikidanych:';
var PROMPT_PLACEHOLDER = 'np. Q123456';
var INVALID_QID = 'Nie jest to poprawny identyfikator. Po kliknięciu „Dalej” zostanie umieszczony zwykły link do Wikipedii';
var QID_ARTICLE_EXISTS = 'Element Wikidanych <a href="//wikidata.org/wiki/$1" target="_blank">$1</a> jest połączony z artykułem <a href="/wiki/$3" target="_blank">$2</a>. Jeśli przejdziesz dalej, cel linku w tym miejscu zostanie zmieniony, tak aby prowadzić do tego artykułu.';
var ACCEPT_BTN = 'Zatwierdź';
var UNLINK_BTN = 'Usuń link';
var NEXT_BTN = 'Dalej';
var BACK_BTN = 'Wstecz';
var CANCEL_BTN = 'Anuluj';
var SUGGESTION_HEADER = 'Sugestie';
var SUGGESTION_EXT_TITLE = 'Zobacz ten element w Wikidanych';
var SUGGESTION_LOADING = 'Wczytywanie...';
var SUGGESTION_NO_RESULTS = 'Brak sugestii';
var SUGGESTION_NODESC = 'brak opisu';
var SUGGESTION_ERROR = 'Wystąpił błąd: $1';
var SUGGESTION_UNAVAILABLE = 'Sugestie nie są dostępne';
var SUGGESTION_LOAD_MORE = 'Wczytaj więcej';
var CONFIRM_WIKITEXT_TEXT = '<div>Czy szablon został wstawiony w poprawnym miejscu? W rzadkich przypadkach skrypt może błędnie zlokalizować link do zamiany.<pre>$1</pre></div>';
var DISCARD_CONFIRM_TEXT = 'Czy na pewno chcesz odrzucić wszystkie zmiany?';
var ERROR_TEXT = 'Nie udało się zlokalizować linku w wikikodzie. Dokonaj zmian ręcznie.';
var EDIT_SUMMARY = {
intro: 'Czerwone linki: ',
template: 'zamieniono $1 na szablon {{[[Szablon:Link-interwiki|Link-interwiki]]}}',
localLink: 'zamieniono $1 na lokalny link',
unlink: 'usunięto $1'
};
var ERROR_TITLE = 'Wystąpił błąd';
var ITEM_NOT_EXISTS = 'Element Wikidanych $1 nie istnieje.';
var API_CONFIG = {
parameters: {
format: 'json',
formatversion: 2,
errorformat: 'html',
errorlang: mw.config.get('wgUserLanguage'),
errorsuselocal: true,
maxage: 300,
smaxage: 300,
},
userAgent: 'Gadget-wstaw-link-interwiki'
};
var QID_REGEX = /^Q?\d+$/i;
// These store the wikitext representing the page after any modifications
// The `actualWikitext` will actually be saved to the page
// The `sanitizedWikitext` is the original wikitext with some strings escaped
// Both variables should always have the same length so that index-based searches work
var actualWikitext;
var sanitizedWikitext;
/** The revision id of the wikitext being edited (used for edit conflicts discovery) */
var wikitextRevision;
/** A promise used for waiting for fetching the wikitext */
var wikitextPromise;
/** Number of changes already made */
var numberOfChanges = 0;
/** @type {{[key in ChangeType]: number}} Number of changes per type */
var changeTypes = {
template: 0,
localLink: 0,
unlink: 0
}
/** Total number of red links present on the page */
var totalRedLinks = 0;
/** The element with save and discard buttons */
var saveBox;
var counterBox;
/** Links that have been used but may need to be restored upon discard */
var hiddenLinks = [];
/** Prevents from closing the page if there are unsaved changes */
var windowCloseConfirm;
/**
* Traverse the DOM tree and find all redlinks.
* @returns {HTMLAnchorElement[]} An array of redlinks
*/
function getRedlinks() {
var navboxLinks = document.querySelectorAll('.mw-parser-output .NavFrame a.new, .mw-parser-output .navbox a.new');
navboxLinks.forEach(function(link) {
link.setAttribute(LINK_FROM_TEMPLATE, 'true');
});
/** @type {NodeListOf<HTMLAnchorElement>} */
var domLinks = document.querySelectorAll('.mw-parser-output a.new:not([' + LINK_FROM_TEMPLATE + '])');
var redlinks = [];
var targets = Object.create(null);
for (var i = 0; i < domLinks.length; i++) {
var link = domLinks[i];
var nextSibling = link.nextElementSibling;
if(nextSibling){
// If the next sibling has one of those classes, it's generated by {{link-interwiki}}
// Therefore skip it
if(nextSibling.classList.contains('link-interwiki') || nextSibling.classList.contains('extiw')){
continue;
}
}
// Set the normalized target page and link index as data attributes
var targetPage = extractPageTitle(link.href);
targetPage = normalizePageName(targetPage);
if (getNamespace(targetPage) !== 0) {
// We only want to process main namespace links
continue;
}
var targetIndex = targets[targetPage] || 0;
targets[targetPage] = targetIndex + 1;
link.setAttribute(LINK_TARGET_INDEX, targetIndex);
redlinks.push(link);
}
return redlinks;
}
/**
* Displays a link that invokes the script on a given link.
* @param {HTMLAnchorElement[]} redlinks An array of redlinks
* @returns {void}
*/
function displayLinks(redlinks) {
redlinks.forEach(function(redlink) {
var link = document.createElement('a');
link.href = 'javascript:void(0)';
link.innerHTML = '<sub class="insert-interwiki">Q</sub>';
link.title = LINK_TITLE;
link.addEventListener('click', function(e) {
invoke(redlink).then(function(changeType){
numberOfChanges++;
changeTypes[changeType]++;
updateSaveBox();
link.style.display = 'none';
hiddenLinks.push(link);
}).fail(function(error){
if(!error) return;
mw.notify(error, {autoHideSeconds: 'long', title: ERROR_TITLE, type: 'error'});
});
e.preventDefault();
});
if(redlink.parentNode){
redlink.parentNode.insertBefore(link, redlink.nextSibling);
}
});
}
/**
* Invokes the script and asks user for input.
* @param {HTMLAnchorElement} link The link to invoke the script on
* @returns {JQuery.Promise<ChangeType, JQuery | string | null>} A promise that resolves when the link has been added or failed
*/
function invoke(link){
var deferred = $.Deferred();
var page = mw.config.get('wgPageName');
// Download the wikitext if not already done
if(!wikitextPromise){
wikitextPromise = getPageWikitext(page).done(function(revision){
wikitextRevision = revision.revisionId;
actualWikitext = revision.wikitext;
sanitizedWikitext = sanitizeWikitext(actualWikitext);
});
wikitextPromise.fail(function (error){
deferred.reject(error);
});
}
var localTitle = extractPageTitle(link.href);
var targetIndex = parseInt(link.getAttribute(LINK_TARGET_INDEX) || '0');
var linkText = link.innerText;
mw.loader.using(['oojs-ui-core', 'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.api', 'mediawiki.confirmCloseWindow']).then(function(){
var promptPromise = displayQidPopup(link, function(qid){
var isWikidata = qid ? QID_REGEX.test(qid) : false;
return createNewWikitext(localTitle, linkText, qid, isWikidata, targetIndex);
}, localTitle);
promptPromise.done(function(data){
actualWikitext = data.actualWikitext;
sanitizedWikitext = data.sanitizedWikitext;
deferred.resolve(data.changeType);
}).fail(function(error){
deferred.reject(error);
});
});
return deferred.promise();
}
/**
* Creates a new wikitext, by replacing the link with a link to specified page.
* @param {string} localTitle The human-friendly local page name
* @param {string} linkText The visible link text
* @param {string | null} linkTarget The target page name; pass null to delete the link
* @param {boolean} isWikidata Whether the link is to Wikidata (`false` creates ordinary local link)
* @param {number} targetIndex The index of the link to the target page
* @returns {WikitextResult | null} Data about the new wikitext or null if the link could not be found
*/
function createNewWikitext(localTitle, linkText, linkTarget, isWikidata, targetIndex){
var newText;
if (linkTarget !== null) {
newText = prepareTemplate(localTitle, linkText, linkTarget, isWikidata);
} else {
newText = linkText;
}
var replaced = replaceLinkWithText(actualWikitext, sanitizedWikitext, localTitle, targetIndex, newText);
if(!replaced) return null;
var excerptForUser = makeExcerpt(
replaced.newWikitext, replaced.indexFrom, replaced.indexTo, 30);
excerptForUser = excerptForUser.replace(/</g, '<');
/** @type {ChangeType} */
var changeType = 'template';
if (linkTarget === null) {
changeType = 'unlink';
} else if (!isWikidata) {
changeType = 'localLink';
}
return {
actualWikitext: replaced.newWikitext,
sanitizedWikitext: replaced.newSanitizedWikitext,
excerpt: excerptForUser,
changeType: changeType
}
}
/**
* Returns a promise that resolves to the wikitext of the page.
* @param {string} page The page name
* @returns {JQuery.Promise<{wikitext: string, revisionId: number}, JQuery>} The page's wikitext
*/
function getPageWikitext(page){
var deferred = $.Deferred();
var api = new mw.Api(API_CONFIG);
api.get({
action: 'query',
prop: 'revisions',
titles: page,
rvprop: ['content', 'ids'],
rvslots: 'main'
}).then(function(data){
var page = data.query.pages[0];
var revision = page.revisions[0];
var slot = revision.slots.main;
var wikitext = slot.content;
var revid = revision.revid;
deferred.resolve({
wikitext: wikitext,
revisionId: revid
});
}).fail(function(code, result){
deferred.reject(api.getErrorMessage(result));
});
return deferred.promise();
}
/**
* Extracts the page title from link href.
* @param {string} href The link href
* @returns {string} The page title
*/
function extractPageTitle(href){
var match;
if(href.indexOf('/wiki/') !== -1){
match = href.match(/\/wiki\/([^?]+)/);
}else{
match = href.match(/[?&]title=([^&]+)(&|$)/);
}
if(match === null) return '';
var title = match[1]
title = decodeURIComponent(title);
title = title.replace(/_/g, ' ');
return title;
}
/**
* Replaces a link with the specified text.
* @param {string} actualWikitext The actual wikitext to replace the link in
* @param {string} sanitizedWikitext The sanitized wikitext to replace the link in
* @param {string} targetPage The link target page
* @param {number} linkIndex The ordinal number of the link among others with the same target
* @param {string} textToReplace A text to be inserted in place of the link
* @returns {LinkReplaceState | null} The modified wikitext or null if the replace failed
*/
function replaceLinkWithText(actualWikitext, sanitizedWikitext, targetPage, linkIndex, textToReplace){
// Link is a text in the form of [[targetPage|linkText]]trail
// where linkText and trail are optional,
// targetPage cannot contain a character from set: []{}<>|
// linkText cannot contain a ] character
// trail can only be letters
var matches = Array.from(
sanitizedWikitext.matchAll(/\[\[([^\[\]{}<>|]+)(\|[^\]]+)?\]\]([a-zA-ZąćęłńóśźżĄĆĘŁŃÓŚŹŻ]*)/g));
targetPage = normalizePageName(targetPage);
for(var i = 0; i < matches.length; i++){
var match = matches[i];
var matchIndex = match.index;
if(matchIndex === undefined) continue;
if(normalizePageName(match[1]) === targetPage){
if(linkIndex > 0){
linkIndex--;
continue;
}
if(textToReplace.length < match[0].length + 3) {
// 0x18 (CANCEL in ASCII) will be then stripped out
textToReplace = textToReplace.padEnd(match[0].length + 3, '\x18');
}
// Replace the link with the text
actualWikitext = actualWikitext.substring(0, matchIndex) +
textToReplace + actualWikitext.substring(matchIndex + match[0].length);
// Prepare a placeholder for sanitized wikitext: [[targetPage|linkText|<...>]]trail
var synchronizedPlaceholder = match[0].substring(0, match[0].length - match[3].length - 2);
synchronizedPlaceholder += '|<';
synchronizedPlaceholder += '.'.repeat(textToReplace.length - match[0].length - 3);
synchronizedPlaceholder += '>]]' + match[3];
// Synchronize the sanitized wikitext
sanitizedWikitext = sanitizedWikitext.substring(0, matchIndex) +
synchronizedPlaceholder + sanitizedWikitext.substring(matchIndex + match[0].length);
return {
newWikitext: actualWikitext,
newSanitizedWikitext: sanitizedWikitext,
indexFrom: matchIndex,
indexTo: matchIndex + textToReplace.length
};
}
}
return null;
}
/**
* Removes references, comments and nowiki tags from the wikitext and replaces them
* with placeholders of the same length.
* @param {string} wikitext The wikitext to sanitize
*/
function sanitizeWikitext(wikitext){
wikitext = wikitext.replace(/<nowiki *>.*?<\/nowiki *>/gi, function(match){
return '<' + 'X'.repeat(match.length - 2) + '>';
});
wikitext = wikitext.replace(/<!--.*?-->/gi, function(match){
return '<' + 'X'.repeat(match.length - 2) + '>';
});
// Transform [[File:]] syntax so that links inside are no longer "links in links" that break our regex
wikitext = wikitext.replace(/\[\[(Plik|File|Grafika|Image):/gi, '(($1:');
// References may contain links that are displayed out-of-order - at the end of article
// That's why we need to ignore them as well
wikitext = wikitext.replace(/<ref[^\/>]*>.*?<\/ref *>/gi, function(match){
return '<' + 'X'.repeat(match.length - 2) + '>';
});
return wikitext;
}
/**
* Transforms the page name to uppercase with spaces replaced with underscores.
* @param {string} page The page name
*/
function normalizePageName(page){
return page.toUpperCase().replace(/ /g, '_');
}
/**
* Returns the namespace number that corresponds to the page title.
* @param {string} title The page name
* @returns {number} The namespace number corresponding to the page title
*/
function getNamespace(title){
var parts = title.split(':');
if(parts.length == 1){
return 0; // Default is main namespace
}
var namespaceName = parts[0].toLowerCase().replace(/ /g, '_');
var namespaces = mw.config.get('wgNamespaceIds');
if (namespaces[namespaceName] !== undefined) {
return namespaces[namespaceName];
}
return 0;
}
/**
* Prepares the template for an interlanguage link.
* @param {string} localArticle The local article name
* @param {string} displayedText The text displayed in the link
* @param {string} linkTarget The linked article name
* @param {boolean} isWikidata Whether the link is to Wikidata (`false` creates ordinary local link)
* @returns {string}
*/
function prepareTemplate(localArticle, displayedText, linkTarget, isWikidata){
// If local article title and displayed text differ only in first letter case,
// use the display name as the local article title
if(localArticle.substring(1) === displayedText.substring(1)
&& localArticle[0].toUpperCase() === displayedText[0].toUpperCase()){
localArticle = displayedText;
}
if(!isWikidata){
if(linkTarget == displayedText) return '[[' + linkTarget + ']]';
return '[[' + linkTarget + '|' + displayedText + ']]';
}
// Wikidata link
// Capitalize the Q
linkTarget = linkTarget.toUpperCase();
if(linkTarget[0] !== 'Q'){
linkTarget = 'Q' + linkTarget;
}
var templateText = '{{link-interwiki |';
// Just in case
if(localArticle.indexOf('=') !== -1) templateText += '1=';
templateText += localArticle;
if(displayedText !== localArticle){
templateText += ' |tekst=' + displayedText;
}
templateText += ' |Q=' + linkTarget + '}}';
return templateText;
}
/**
* Makes an excerpt focusing on the specified range.
* @param {string} text The original text
* @param {number} indexFrom The index of the first significant character
* @param {number} indexTo The index of the last significant character
* @param {number} margins The number of characters to include before and after the selection
* @returns {string} The excerpt with significant text and some margin
*/
function makeExcerpt(text, indexFrom, indexTo, margins){
var excerptStart = Math.max(0, indexFrom - margins);
var excerptEnd = Math.min(text.length, indexTo + margins);
var excerpt = text.substring(excerptStart, excerptEnd);
excerpt = excerpt.replace(/\x18/g, '');
if(excerptStart > 0) excerpt = '...' + excerpt;
if(excerptEnd < text.length) excerpt += '...';
return excerpt;
}
/**
* Updates the counter in the save box.
* If necessary, the box is created.
*/
function updateSaveBox(){
if(!saveBox){
saveBox = $('<div class="insert-link-interwiki-savebox"></div>');
saveBox.appendTo(document.body);
counterBox = $('<div class="counter"></div>');
counterBox.appendTo(saveBox);
var diffCheckbox = new OO.ui.CheckboxInputWidget({
selected: mw.storage.get(STORAGE_DIFF_KEY) === '1'
});
var diffField = new OO.ui.FieldLayout(diffCheckbox, {
label: SHOW_DIFF_CHECKBOX,
align: 'inline'
});
diffField.$element.appendTo(saveBox);
var buttonDiscard = new OO.ui.ButtonWidget({
label: DISCARD_BUTTON
});
buttonDiscard.on('click', discard);
saveBox.append(buttonDiscard.$element);
var buttonSave = new OO.ui.ButtonWidget({
label: SAVE_BUTTON,
flags: ['primary', 'progressive']
});
buttonSave.on('click', function(){
buttonDiscard.setDisabled(true);
buttonSave.setDisabled(true);
buttonSave.setLabel(SAVING_TEXT);
var displayDiff = diffCheckbox.isSelected();
mw.storage.set(STORAGE_DIFF_KEY, displayDiff ? '1' : '0');
if (displayDiff) {
displayPreview();
return;
} else {
save().then(function(){
location.reload();
}).fail(function(error){
buttonDiscard.setDisabled(false);
buttonSave.setDisabled(false);
buttonSave.setLabel(SAVE_BUTTON);
mw.notify(error, {autoHideSeconds: 'long', title: ERROR_TITLE, type: 'error'});
});
}
});
saveBox.append(buttonSave.$element);
}
counterBox.text(mw.format(COUNTER_TEXT, numberOfChanges, totalRedLinks));
//@ts-ignore
if(!windowCloseConfirm) windowCloseConfirm = mw.confirmCloseWindow();
}
/**
* Reads the current actualWikitext and prepares it for saving
* by stripping unnecessary padding characters.
* @returns {string}
*/
function prepareWikitextForSaving() {
var wikitext = actualWikitext;
wikitext = wikitext.replace(/\x18/g, '');
return wikitext;
}
/**
* Saves the changes
* @returns {JQuery.Promise}
*/
function save(){
var deferred = $.Deferred();
var wikitext = prepareWikitextForSaving();
var page = mw.config.get('wgPageName');
var api = new mw.Api(API_CONFIG);
api.postWithEditToken({
action: 'edit',
title: page,
text: wikitext,
summary: makeEditSummary(changeTypes),
minor: true,
bot: hasBotFlag(),
nocreate: true,
watchlist: 'nochange',
baserevid: wikitextRevision
}).then(function(){
if(windowCloseConfirm) windowCloseConfirm.release();
windowCloseConfirm = undefined;
deferred.resolve();
}).fail(function(code, result){
deferred.reject(api.getErrorMessage(result));
});
return deferred.promise();
}
/**
* Reloads the page with a standard diff view.
* In order to do so, a fake edit form is generated and submitted.
* @returns {void}
*/
function displayPreview() {
// First, check the watchlist status of the current page, because MediaWiki is so dumb
// that edit interface doesn't have option "nochange" for watchlist
var apiParams = {
action: 'query',
prop: 'info',
titles: mw.config.get('wgPageName'),
inprop: 'watched',
};
var api = new mw.Api(API_CONFIG);
var actuallyDisplayPreview = function(isPageWatched, watchlistExpiry){
var wikitext = prepareWikitextForSaving();
var formParams = {
wpTextbox1: wikitext,
wpDiff: 'wpDiff',
wpSummary: makeEditSummary(changeTypes),
wpMinoredit: '1',
wpUnicodeCheck: 'ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ',
wpIgnoreBlankSummary: '1',
wpWatchthis: isPageWatched ? '1' : undefined,
wpWatchlistExpiry: watchlistExpiry,
wpUltimateParam: '1',
};
// Generate the form
var form = document.createElement('form');
form.method = 'POST';
form.action = mw.util.getUrl(mw.config.get('wgPageName'), { action: 'submit' });
form.enctype = 'multipart/form-data';
form.style.display = 'none';
document.body.appendChild(form);
Object.entries(formParams).forEach(function(row) {
if (row[1] === undefined) return;
var input = document.createElement('input');
input.type = 'hidden';
input.name = row[0];
input.value = row[1];
form.appendChild(input);
});
// Just to be sure, release the lock not to be disturbed by additional confirm dialogs
if (windowCloseConfirm) windowCloseConfirm.release();
windowCloseConfirm = undefined;
// Submit the form
form.submit();
}
api.get(apiParams).then(function(data){
var isWatched = data.query.pages[0].watched;
var watchlistExpiry = data.query.pages[0].watchlistexpiry;
actuallyDisplayPreview(isWatched, watchlistExpiry);
}).fail(function(code, result){
// It's not a crucial information so fail silently
var isWatched = false;
var watchlistExpiry = undefined;
actuallyDisplayPreview(isWatched, watchlistExpiry);
});
}
/**
* Discards the changes and unsets all the variables.
*/
function discard(){
OO.ui.confirm(DISCARD_CONFIRM_TEXT).done(function(confirmed){
if(!confirmed) return;
wikitextPromise = undefined;
actualWikitext = undefined;
sanitizedWikitext = undefined;
wikitextRevision = undefined;
numberOfChanges = 0;
saveBox.remove();
saveBox = undefined;
if(windowCloseConfirm) windowCloseConfirm.release();
windowCloseConfirm = undefined;
hiddenLinks.forEach(function(link){
link.style.display = '';
});
hiddenLinks = [];
});
}
/**
* Asks the user to enter a QID and displays a popup with the wikitext.
* @param {HTMLAnchorElement} anchor The anchor element to display the popup next to
* @param {(qid: string|null) => (WikitextResult | null)} makeWikitext A function that returns the wikitexts and excerpt for the specified QID
* @param {string} localTitle Title of the entity to search on Wikidata for suggestions
* @returns {JQuery.Promise<WikitextResult, string>} A promise that resolves to the new wikitexts
*/
function displayQidPopup(anchor, makeWikitext, localTitle){
var deferred = $.Deferred();
// Ask for the QID step
var qidPanel = new OO.ui.PanelLayout({
expanded: false
});
if (anchor.innerText != anchor.innerHTML) {
// A hacky way to warn users that the link label may contain templates
// To handle such a use case properly, the data model would need a significant refactoring
var warningBox = new OO.ui.MessageWidget({
type: 'warning',
label: TEMPLATE_IN_LINK
});
qidPanel.$element.append(warningBox.$element);
}
var fieldset = new OO.ui.FieldsetLayout({});
qidPanel.$element.append(fieldset.$element);
var qidInput = new OO.ui.TextInputWidget({
placeholder: PROMPT_PLACEHOLDER,
autocomplete: false
});
var qidLayout = new OO.ui.FieldLayout(qidInput, {
label: PROMPT_TEXT,
align: 'top'
});
fieldset.addItems([qidLayout]);
var qidUnlinkButton = new OO.ui.ButtonWidget({
label: UNLINK_BTN,
flags: ['destructive'],
});
qidUnlinkButton.$element.css('float', 'left');
var qidNextButton = new OO.ui.ButtonWidget({
label: NEXT_BTN,
flags: ['primary', 'progressive'],
disabled: true
});
var qidCancelButton = new OO.ui.ButtonWidget({
label: CANCEL_BTN
});
var qidButtonLayout = $('<div style="text-align:right; margin-top:8px"></div>');
qidButtonLayout.append(qidUnlinkButton.$element);
qidButtonLayout.append(qidCancelButton.$element);
qidButtonLayout.append(qidNextButton.$element);
qidPanel.$element.append(qidButtonLayout);
// Display suggestions if available
var $suggestionsContainer = renderSuggestionList(
localTitle,
function(qId){ qidInput.setValue(qId); }
);
$suggestionsContainer.insertBefore(qidButtonLayout);
// If specified element is connected to local article
// ask to change to local link instead
var localLinkPanel = new OO.ui.PanelLayout({
expanded: false
});
var localLinkText = $('<div></div>');
localLinkPanel.$element.append(localLinkText);
var localLinkNextButton = new OO.ui.ButtonWidget({
label: NEXT_BTN,
flags: ['primary', 'progressive']
});
var localLinkBackButton = new OO.ui.ButtonWidget({
label: BACK_BTN
});
var localLinkButtonLayout = $('<div style="text-align:right; margin-top:8px"></div>');
localLinkButtonLayout.append(localLinkBackButton.$element);
localLinkButtonLayout.append(localLinkNextButton.$element);
localLinkPanel.$element.append(localLinkButtonLayout);
// Confirm the wikitext step
var wikitextPanel = new OO.ui.PanelLayout({
expanded: false
});
var promptText = $('<div></div>');
wikitextPanel.$element.append(promptText);
var wikitextAcceptButton = new OO.ui.ButtonWidget({
label: ACCEPT_BTN,
flags: ['primary', 'progressive'],
});
var wikitextBackButton = new OO.ui.ButtonWidget({
label: BACK_BTN
});
var wikitextButtonLayout = $('<div style="text-align:right; margin-top:8px"></div>');
wikitextButtonLayout.append(wikitextBackButton.$element);
wikitextButtonLayout.append(wikitextAcceptButton.$element);
wikitextPanel.$element.append(wikitextButtonLayout);
var contentStack = new OO.ui.StackLayout({
expanded: false,
items: [qidPanel, localLinkPanel, wikitextPanel]
});
contentStack.setItem(qidPanel);
var $popupContainer = $('body');
var popup = new OO.ui.PopupWidget({
$container: $popupContainer,
$floatableContainer: $(anchor),
$content: contentStack.$element,
padded: true,
width: 350,
head: true,
label: QID_POPUP_TITLE,
hideCloseButton: true,
autoFlip: true,
classes: ['insert-link-interwiki-popup']
});
$popupContainer.append(popup.$element);
popup.toggle(true);
var newWikitext;
qidInput.on('change', function(){
var qid = qidInput.getValue();
qidNextButton.setDisabled(qid.length === 0);
if(!QID_REGEX.test(qid) && qid.length > 0){
qidLayout.setWarnings([INVALID_QID]);
}else{
qidLayout.setWarnings([]);
}
});
qidNextButton.on('click', function(){
var enteredQid = qidInput.getValue();
qidInput.pushPending();
getLocalArticleForQid(enteredQid).then(function(localArticle){
qidInput.popPending();
newWikitext = makeWikitext(localArticle || enteredQid);
if(!newWikitext){
popup.toggle(false);
return deferred.reject(ERROR_TEXT);
}
var message = mw.format(CONFIRM_WIKITEXT_TEXT, newWikitext.excerpt);
promptText.empty();
promptText.append(
$(message)
);
if (localArticle === null) {
contentStack.setItem(wikitextPanel);
} else {
var urlencodedTitle = encodeURIComponent(localArticle);
localLinkText.html(mw.format(QID_ARTICLE_EXISTS, enteredQid, localArticle, urlencodedTitle));
localLinkNextButton.setDisabled(false);
contentStack.setItem(localLinkPanel);
}
}).fail(function(reason) {
qidInput.popPending();
localLinkText.html(reason);
localLinkNextButton.setDisabled(true);
contentStack.setItem(localLinkPanel);
});
});
qidCancelButton.on('click', function(){
popup.toggle(false);
deferred.reject(null);
});
qidUnlinkButton.on('click', function(){
newWikitext = makeWikitext(null);
popup.toggle(false);
if (!newWikitext) {
deferred.reject(ERROR_TEXT);
} else {
anchor.style.color = 'inherit';
deferred.resolve(newWikitext);
}
});
localLinkNextButton.on('click', function(){
contentStack.setItem(wikitextPanel); // Everything has been prepared in the previous step
});
localLinkBackButton.on('click', function(){
contentStack.setItem(qidPanel);
});
wikitextAcceptButton.on('click', function(){
popup.toggle(false);
deferred.resolve(newWikitext);
});
wikitextBackButton.on('click', function(){
contentStack.setItem(qidPanel);
});
return deferred.promise();
}
/**
* Renders a list of item suggestions
* @param {string} localTitle Title for which to search for suggestions
* @param {(qId: string) => void} onSelect A function that is called when the user selects a suggestion (with QId as parameter)
* @returns {JQuery<HTMLElement>}
*/
function renderSuggestionList(localTitle, onSelect){
var searchContinue = null;
var searchPromise = getItemSuggestions(localTitle, 0, 4);
var $suggestionsContainer = $('<div class="suggestions"></div>');
if (!searchPromise){
$suggestionsContainer.append('<div class="error">' + SUGGESTION_UNAVAILABLE + '</div>');
return $suggestionsContainer;
}
$suggestionsContainer.append('<div class="header">' + SUGGESTION_HEADER + '</div>');
var $emptyState = $('<div class="empty-state">' + SUGGESTION_LOADING + '</div>');
$suggestionsContainer.append($emptyState);
searchPromise.done(function(searchResult){
var suggestions = searchResult.items;
searchContinue = searchResult.continue;
if(suggestions.length === 0){
$emptyState.text(SUGGESTION_NO_RESULTS);
return;
}
$emptyState.remove();
var $suggestionsList = $('<ul></ul>');
suggestions.forEach(function(suggestion) {
addSuggestionItem($suggestionsList, suggestion);
});
$suggestionsContainer.append($suggestionsList);
if (searchContinue) {
var $loadMoreButton = $('<a href="javascript:void(0)" class="load-more">' + SUGGESTION_LOAD_MORE + '</a>');
$suggestionsContainer.append($loadMoreButton);
$loadMoreButton.on('click', function(){
$loadMoreButton.prop('disabled', true);
var searchPromise = getItemSuggestions(localTitle, searchContinue, 8);
if (!searchPromise){
$loadMoreButton.prop('disabled', false);
return;
}
searchPromise.then(function(searchResult){
searchContinue = searchResult.continue;
var suggestions = searchResult.items;
suggestions.forEach(function(suggestion) {
addSuggestionItem($suggestionsList, suggestion);
});
$loadMoreButton.prop('disabled', false);
if (!searchContinue) {
$loadMoreButton.remove();
}
}).fail(function(){
$loadMoreButton.prop('disabled', false);
});
});
}
}).fail(function(error){
$emptyState.text(mw.format(SUGGESTION_ERROR, error));
});
/** @type {(HTMLElement, WikidataSuggestion) => void} */
var addSuggestionItem = function($suggestionsList, suggestion) {
// Prepare the list item
var $suggestionItem = $('<li></li>');
var $mainButton = $('<button type="button"></button>');
$('<span class="label"></span>')
.text(suggestion.label + ' (' + suggestion.id + ')')
.appendTo($mainButton);
$('<span class="description"></span>')
.text(suggestion.description || SUGGESTION_NODESC)
.appendTo($mainButton);
$mainButton.on('click', function(){
onSelect(suggestion.id);
});
$suggestionItem.append($mainButton);
// Add a link to Wikidata
var $wikidataLink = $('<a></a>');
$wikidataLink.attr('href', 'https://www.wikidata.org/wiki/' + suggestion.id);
$wikidataLink.attr('target', '_blank');
$wikidataLink.attr('title', SUGGESTION_EXT_TITLE);
$wikidataLink.append('<img src="https://upload.wikimedia.org/wikipedia/commons/6/67/OOjs_UI_icon_external-link-ltr.svg" class="skin-invert" />');
$suggestionItem.append($wikidataLink);
$suggestionsList.append($suggestionItem);
};
return $suggestionsContainer;
}
/**
* Returns a list of suggestions for the specified title.
* @param {string} title The title of the item to search for
* @param {number} skipResults The number of results to skip (for pagination)
* @param {number} limit The maximum number of results to return
* @returns {JQuery.Promise<{ items: WikidataSuggestion[], continue: number | undefined }, string> | null} A promise that resolves to an array of suggestions or rejects with an error message (or null if unavailable)
*/
function getItemSuggestions(title, skipResults, limit){
var deferred = $.Deferred();
if(!mw.ForeignApi){
return null;
}
// Strip the disambiguation part from the title
title = title.replace(/([ _]*\([^)]+\))/g, '');
var api = new mw.ForeignApi('https://www.wikidata.org/w/api.php', API_CONFIG);
api.get({
action: 'wbsearchentities',
type: 'item',
search: title,
language: mw.config.get('wgContentLanguage'),
uselang: mw.config.get('wgContentLanguage'),
limit: limit,
continue: skipResults,
formatversion: 2
}).then(function(data){
var results = data.search || [];
var suggestions = [];
for(var i = 0; i < results.length; i++){
var result = results[i];
suggestions.push({
label: result.label,
description: result.description,
id: result.id
});
}
deferred.resolve({
items: suggestions,
continue: data['search-continue']
});
}).fail(function(code, result){
deferred.reject(api.getErrorMessage(result));
});
return deferred.promise();
}
/**
* Fetches the local article attached to the specified Wikidata item.
* Returns null if the item does not exist or is not connected to the local wiki article.
* @param {string} qid
* @returns {JQuery.Promise<string | null>}
*/
function getLocalArticleForQid(qid){
var deferred = $.Deferred();
// Invalid QIDs can't have local articles
if(!QID_REGEX.test(qid)){
deferred.resolve(null);
return deferred.promise();
}
if(!mw.ForeignApi){
deferred.resolve(null);
return deferred.promise();
}
var api = new mw.ForeignApi('https://www.wikidata.org/w/api.php', API_CONFIG);
var dbname = mw.config.get('wgDBname');
api.get({
action: 'wbgetentities',
ids: qid,
props: 'sitelinks',
sitefilter: dbname,
language: mw.config.get('wgContentLanguage'),
uselang: mw.config.get('wgContentLanguage'),
formatversion: 2
}).then(function(data){
var result = data.entities[qid];
if (!result) {
deferred.reject(mw.format(ITEM_NOT_EXISTS, qid));
return;
}
var sitelinks = result.sitelinks;
if (!sitelinks || !sitelinks[dbname]) {
deferred.resolve(null);
return;
}
var localTitle = sitelinks[dbname].title;
deferred.resolve(localTitle);
}).fail(function(code, result){
if (code === 'no-such-entity') {
deferred.reject(mw.format(ITEM_NOT_EXISTS, qid));
}
deferred.reject(api.getErrorMessage(result));
});
return deferred.promise();
}
/**
* Checks if the user can mark their edits as bot.
* @returns {boolean}
*/
function hasBotFlag(){
var flagGroups = ['bot', 'flood'];
var userGroups = mw.config.get('wgUserGroups');
for(var i = 0; i < flagGroups.length; i++){
if(userGroups.indexOf(flagGroups[i]) !== -1){
return true;
}
}
return false;
}
/**
* Prepares an edit summary based on the number of different changes.
* @param {{[key in ChangeType]: number}} changeTypes Number of changes for each type
* @returns {string} The edit summary
*/
function makeEditSummary(changeTypes) {
var summaryParts = [];
for (var type in changeTypes) {
if (changeTypes[type] == 0) continue;
summaryParts.push(mw.format(EDIT_SUMMARY[type], changeTypes[type]));
}
return EDIT_SUMMARY.intro + summaryParts.join(', ');
}
// Initialize only on editable pages in read mode
if(!mw.config.get('wgIsProbablyEditable')) return;
if(mw.config.get('wgAction') !== 'view') return;
var ns = mw.config.get('wgNamespaceNumber');
var pageTitle = mw.config.get('wgPageName');
var isSubpage = pageTitle.indexOf('/') !== -1;
var redLinks = getRedlinks();
totalRedLinks = redLinks.length;
if(totalRedLinks === 0) return;
if(ns == 0 || (ns == 2 && isSubpage)){
displayLinks(redLinks);
} else {
var link = mw.util.addPortletLink('p-tb', 'javascript:void(0)', TRIGGER_TEXT, 't-link-interwiki', TRIGGER_TOOLTIP);
link.addEventListener('click', function(){
displayLinks(redLinks);
});
}
});
/**
* Describes the new wikitext and place where the modification was made.
* Indices refer to the new wikitext.
* @typedef {{
* newWikitext: string,
* newSanitizedWikitext: string,
* indexFrom: number,
* indexTo: number,
* }} LinkReplaceState
*
* Describes a suggestion for an item retrieved from Wikidata.
* @typedef {{
* id: string,
* label: string | undefined,
* description: string | undefined
* }} WikidataSuggestion
*
* @typedef {{
* actualWikitext: string,
* sanitizedWikitext: string,
* excerpt: string,
* changeType: ChangeType
* }} WikitextResult
*
* @typedef {'template' | 'localLink' | 'unlink'} ChangeType
*/
// </nowiki>