MediaWiki:AFC-submit-wizard.js: Difference between revisions

Content deleted Content added
improve variable naming
m SD0001 moved page User:SD0001/AFC-submit-wizard.js to MediaWiki:AFC-submit-wizard.js: move to MediaWiki namespace so that it doesn't need to load the script from userspace
 
(22 intermediate revisions by 2 users not shown)
Line 1:
/**
* MediaWiki:AFCAfC-submit-wizard.js
*
* JavaScript used for submitting drafts to AfC.
* Used on [[Wikipedia:Articles for creation/Submitting]].
* Loaded via [[mw:Snippets/Load JS and CSS by URL]].
*
* Edits can be proposed via GitHub (https://github.com/wikimedia-gadgets/afc-submit-wizard)
* or a talk page request.
*
* Author: [[User:SD0001]]
Line 47 ⟶ 50:
"fieldset-label": "Submit your draft for review at Articles for Creation (AfC)",
"title-label": "Draft title",
"title-placeholder": "Enter the draft titlepage name, usually begins with \"Draft:\" or \"User:\"",
"title-helptip": "This should be pre-filled if you clicked the link while on the draft page",
"rawclass-label": "Choose the most appropriate category",
Line 61 ⟶ 64:
"orestopic-helptip": "Pick the topic areas that are relevant",
"submit-label": "Submit",
"footer-text": "<small>If you are not sure about what to enter in a field, you can skip it. If you need further help, you can ask at the <b>[[WP:AFCHD|AfC help desk]]</b> or get live help via <b>[[WP:IRCHELP|liveIRC]]</b> helpor <b>[[WP:DISCORD|Discord]]</b>. <br>Facing some issues in using this form? <b>[[WT/w/index.php?title=Wikipedia_talk:WPAFC|WikiProject_Articles_for_creation/Submission_wizard&action=edit&section=new&preloadtitle=Issue%20with%20submission%20form&editintro=Wikipedia_talk:WikiProject_Articles_for_creation/Submission_wizard/editintro Report it]]</b>.</small>",
"submitting-as": "Submitting as User:$1",
"validation-notitle": "Please enter the draft page name",
"validation-invalidtitle": "Please check draft title. This title is invalid.",
"validation-missingtitle": "Please check draft title. No such draft exists.",
Line 71 ⟶ 75:
"editsummary-main": "Submitting using [[WP:AFCSW|AfC-submit-wizard]]",
"status-redirecting": "Submission succeeded. Redirecting you to the draft page ...",
"captcha-label": "Please enter the letters appearing in the box below",
"captcha-placeholder": "Enter the letters here",
"captcha-helptip": "CAPTCHA security check. Click \"Submit\" again when done.",
"error-saving-main": "An error occurred ($1). Please try again or refer to the help desk.",
"status-saving-talk": "Saving draft talk page ...",
Line 86 ⟶ 93:
document.title = msg('document-title');
$('#firstHeading').text(msg('page-title'));
mw.util.addCSS(
// CSS adjustments for vector-2022: hide prominent page controls which are
// irrelevant and confusing while using the wizard
'.vector-page-toolbar { display: none } ' +
'.vector-page-titlebar #p-lang-btn { display: none } ' +
// Hide categories as well, prevents accidental HotCat usage
'#catlinks { display: none } '
);
 
var apiOptions = {
Line 114 ⟶ 131:
items: [
ui.titleLayout = new OO.ui.FieldLayout(ui.titleInput = new mw.widgets.TitleInputWidget({
value: (mw.util.getParamValue('draftpage') || '').replace(/_/g, ' '),
placeholder: msg('title-placeholder'),
}), {
Line 175 ⟶ 192:
label: msg('submit-label'),
flags: [ 'progressive', 'primary' ],
})),
 
ui.footerLayout = new OO.ui.FieldLayout(new OO.ui.LabelWidget({
label: $('<div>')
.append(linkify(msg('footer-text')))
}), {
align: 'top'
}),
]
});
 
ui.footerLayout = new OO.ui.FieldLayout(new OO.ui.LabelWidget({
label: $('<div>')
.append(linkify(msg('footer-text')))
}), {
align: 'top'
});
 
Line 217 ⟶ 234:
 
// Attach
$('#afc-submit-wizard-container').empty().append(ui.fieldset.$element, ui.footerLayout.$element);
mw.track('counter.gadget_afcsw.opened');
 
// Populate talk page tags for multi-select widget
Line 228 ⟶ 247:
}));
});
 
ui.clearTalkTags = function () {
afc.talkTagOptionsLoaded.then(function () {
ui.talkTagsInput.setValue([]);
});
};
ui.addTalkTags = function (tags) {
afc.talkTagOptionsLoaded.then(function () {
ui.talkTagsInput.setValue(ui.talkTagsInput.getValue().concat(tags));
});
};
 
// Get mapping of infoboxes with relevant WikiProjects
afc.ibxmapLoaded = getJSONPage('Wikipedia:WikiProject Articles for creation/Infobox WikiProject map.json');
 
ui.submitButton.on('click', evaluatehandleSubmit);
ui.titleInput.on('change', mw.util.debounce(config.debounceDelay, onDraftInputChange));
 
if (mw.util.getParamValue('draftpage')) {
onDraftInputChange();
}
Line 241 ⟶ 271:
// The default font size in monobook and modern are too small at 10px
mw.util.addCSS('.skin-modern .projectTagOverlay, .skin-monobook .projectTagOverlay { font-size: 130%; }');
 
afc.beforeUnload = function (e) {
e.preventDefault();
e.returnValue = '';
return '';
};
$(window).on('beforeunload', afc.beforeUnload);
}
 
Line 258 ⟶ 295:
afc.talktext = null;
afc.pagetext = null;
ui.clearTalkTags();
 
afc.talkTagOptionsLoaded.then(function () {
// Clear selection
ui.talkTagsInput.setValue([]);
});
 
afc.lookupApi.get({
Line 328 ⟶ 361:
 
// Guess WikiProject tags from infoboxes on the page
$.when(afc.ibxmapLoaded, afc.talkTagOptionsLoaded).then(function (ibxmap) {
var infoboxRgx = /\{\{([Ii]nfobox [^|}]*)/g,
wikiprojects = [],
Line 334 ⟶ 367:
while (match = infoboxRgx.exec(afc.pagetext)) {
var ibx = match[1].trim();
ibx = ibx[0].toUpperCase() + ibx.slice(1);
if (ibxmap[ibx]) {
wikiprojects = wikiprojects.concat(ibxmap[ibx]);
Line 339 ⟶ 373:
}
debug('wikiprojects from infobox: ', wikiprojects);
ui.talkTagsInput.setValue(ui.talkTagsInput.getValue().concataddTalkTags(wikiprojects));
});
 
Line 368 ⟶ 402:
return e.name;
});
afc.talkTagOptionsLoaded.then(function () {
ui.talkTagsInput.setValue(ui.talkTagsInput.getValue().concat(existingTags));
});
 
debug(existingTags);
ui.addTalkTags(existingTags);
}
 
/**
* @param {Object} page - from query API response
* @returns {string[]}
*/
Line 393 ⟶ 424:
 
/**
* @param {Object} page - from query API response
* @returns {string[]}
*/
function warningsFromPageData(page) {
var pagetext = page.revisions[0].slots.main.content;
 
var warnings = [];
 
// Show no refs warning
if (!/<ref/i.test(pagetext) && !/\{\{([Ss]fn|[Hh]arv)/.test(pagetext)) {
warnings.push('warning-norefs');
return [
new OO.ui.HtmlSnippet(linkify(msg('warning-norefs')))
];
}
 
return [];
// TODO: Show warning for use of deprecated/unreliable sources
// TODO: Show tip for avoiding peacock words or promotional language?
 
return warnings.map(function (warning) {
return new OO.ui.HtmlSnippet(linkify(msg(warning)));
});
}
 
/**
* @param {number} revid
* @returns {jQuery.Promise<string[]>}
*/
function getOresTopics(revid) {
return $.get('https://ores.wikimedia.org/v3/scores/enwiki/?models=drafttopic&revids=' + revid).then(function (json) {
Line 433 ⟶ 474:
return topic.split('.').pop();
})
.filter(function (e) {
return e; // filter out undefined from above
})
.map(function (topic) {
// convert topic string to normalised form
return topic
.replace(/[A-Z]/g, function (match) {
return match[0].toLowerCase();
})
.replace(/ /g, '-')
.replace(/&/g, 'and');
});
});
}
 
/***
* @param {string} text
* @returns {{wikitext: string, name: string}[]}
*/
function extractWikiProjectTagsFromText(text) {
if (!text) {
Line 470 ⟶ 515:
}
 
/**
function evaluate() {
* @param {string} type
 
* @param {string} message
*/
function setMainStatus(type, message) {
if (!ui.mainStatusLayout || !ui.mainStatusLayout.isElementAttached()) {
ui.fieldset.addItems([
ui.mainStatusLayout = new OO.ui.FieldLayout(ui.mainStatusArea = new OO.ui.MessageWidget({))
type: 'notice',
label: msg('status-processing')
}))
]);
}
ui.mainStatusArea.setType(type);
ui.mainStatusArea.setLabel(message);
}
 
 
/**
* @param {string} type
* @param {string} message
*/
function setTalkStatus(type, message) {
if (!ui.talkStatusLayout) {
ui.fieldset.addItems([
ui.talkStatusLayout = new OO.ui.FieldLayout(ui.talkStatusArea = new OO.ui.MessageWidget())
]);
}
ui.talkStatusArea.setType(type);
ui.talkStatusArea.setLabel(message);
}
 
function handleSubmit() {
 
setMainStatus('notice', msg('status-processing'));
mw.track('counter.gadget_afcsw.submit_attempted');
ui.submitButton.setDisabled(true);
ui.mainStatusLayout.scrollElementIntoView();
 
var draft = ui.titleInput.getValue();
if (!draft) {
ui.titleLayout.setErrors([msg('validation-notitle')]);
ui.fieldset.removeItems([ui.mainStatusLayout]);
ui.submitButton.setDisabled(false);
ui.titleLayout.scrollElementIntoView();
return;
}
debug(draft);
 
Line 494 ⟶ 568:
"rvslots": "main",
}).then(function (json) {
var pageapiPage = json.query.pages[0];
 
var errors = errorsFromPageData(pageapiPage);
if (errors.length) {
ui.titleLayout.setErrors(errors);
Line 505 ⟶ 579:
}
 
var text = page.revisions[0].slots.main.contentprepareDraftText(apiPage);
 
setMainStatus('notice', msg('status-saving'));
var header = '';
saveDraftPage(draft, text).then(function () {
setMainStatus('success', msg('status-redirecting'));
mw.track('counter.gadget_afcsw.submit_succeeded');
 
$(window).off('beforeunload', afc.beforeUnload);
 
setTimeout(function () {
// Handle short description
___location.href = mw.util.getUrl(draft);
var shortDescTemplateExists = /\{\{[Ss]hort ?desc(ription)?\s*\|/.test(text);
}, config.redirectionDelay);
var shortDescExists = !!page.description;
}, function (code, err) {
var existingShortDesc = page.description;
if (code === 'captcha') {
 
ui.fieldset.removeItems([ui.mainStatusLayout, ui.talkStatusLayout]);
if (ui.shortdescInput.getValue()) {
ui.captchaLayout.scrollElementIntoView();
// 1. No shortdesc - insert the one provided by user
mw.track('counter.gadget_afcsw.submit_captcha');
if (!shortDescExists) {
header += '{{Short description|' + ui.shortdescInput.getValue() + '}}\n';
 
// 2. Shortdesc exists from {{short description}} template - replace it
} else if (shortDescExists && shortDescTemplateExists) {
text = text.replace(/\{\{[Ss]hort ?desc(ription)?\s*\|.*?\}\}\n*/g, '');
header += '{{Short description|' + ui.shortdescInput.getValue() + '}}\n';
 
// 3. Shortdesc exists, but not generated by {{short description}}, if the user
// has changed the value, save the new value
} else if (shortDescExists && existingShortDesc !== ui.shortdescInput.getValue()) {
header += '{{Short description|' + ui.shortdescInput.getValue() + '}}\n';
 
// 4. Shortdesc exists, but not generated by {{short description}}, and user hasn't changed the value
} else {
setMainStatus('error', msg('error-saving-main', makeErrorMessage(code, err)));
// Do nothing
mw.track('counter.gadget_afcsw.submit_failed');
mw.track('counter.gadget_afcsw.submit_failed_' + code);
}
ui.submitButton.setDisabled(false);
} else {
});
// User emptied the shortdesc field (or didn't exist from before): remove any existing shortdesc.
// This doesn't remove any shortdesc that is generated by other templates
// Race condition (FIXME): if someone else added a shortdesc to the draft after this user opened the wizard,
// that shortdesc gets removed
text = text.replace(/\{\{[Ss]hort ?desc(ription)?\s*\|.*?\}\}\n*/g, '');
}
 
var talktext = prepareTalkText(afc.talktext);
 
if (!afc.talktext && !talktext) {
// Draft topics
// No content earlier, no content now. Stop here to avoid
debug(ui.oresTopicInput);
// creating the talk page as empty.
if (ui.oresTopicLayout.isVisible()) {
return;
afc.oresTopics = ui.oresTopicInput.getValue();
}
if (afc.oresTopics.length) {
text = text.replace(/\{\{[Dd]raft topics\|.*?\}\}\n*/g, '');
header += '{{Draft topics|' + afc.oresTopics.join('|') + '}}\n';
}
 
setTalkStatus('notice', msg('status-saving-talk'));
// Add AfC topic
text = text.replace(/\{\{AfC topic\|(.*?)\}\}/g, '');
header += '{{AfC topic|' + ui.afcTopicInput.getValue() + '}}\n';
 
// put AfC submission template
header += '{{subst:submit|' + (mw.util.getParamValue('username') || mw.config.get('wgUserName')) + '}}\n';
 
// insert everything to the top
text = header + text;
debug(text);
 
ui.mainStatusArea.setType('notice');
ui.mainStatusArea.setLabel(msg('status-saving'));
 
// saving draft page
afc.api.postWithEditToken({
"action": "edit",
"title": new mw.Title(draft).getTalkPage().toText(),
"text": texttalktext,
"summary": msg('editsummary-maintalk')
}).then(function (data) {
if (data.edit && data.edit.result === 'Success') {
ui.mainStatusArea.setTypesetTalkStatus('success', msg('status-talk-success'));
ui.mainStatusArea.setLabel(msg('status-redirecting'));
 
setTimeout(function () {
___location.href = mw.util.getUrl(draft);
}, config.redirectionDelay);
} else {
return $.Deferred().reject('unexpected- result');
}
}).catch(function (code, err) {
setTalkStatus('error', msg('error-saving-talk', makeErrorMessage(code, err)));
ui.mainStatusArea.setType('error');
ui.mainStatusArea.setLabel(msg('error-saving-main', makeErrorMessage(code, err)));
ui.submitButton.setDisabled(false);
});
 
ui.fieldset.addItems([
ui.talkStatusLayout = new OO.ui.FieldLayout(ui.talkStatusArea = new OO.ui.MessageWidget({
type: 'notice',
label: msg('status-saving-talk')
}))
]);
 
}).catch(function (code, err) {
setMainStatus('error', msg('error-main', makeErrorMessage(code, err)));
ui.submitButton.setDisabled(false);
mw.track('counter.gadget_afcsw.submit_failed');
mw.track('counter.gadget_afcsw.submit_failed_' + code);
});
 
}
// Process text of the talk page
var alreadyExistingWikiProjects = extractWikiProjectTagsFromText(afc.talktext);
var alreadyExistingTags = alreadyExistingWikiProjects.map(function (e) {
return e.name;
});
var tagsToAdd = ui.talkTagsInput.getValue().filter(function (tag) {
return alreadyExistingTags.indexOf(tag) === -1;
});
var tagsToRemove = alreadyExistingTags.filter(function (tag) {
return ui.talkTagsInput.getValue().indexOf(tag) === -1;
});
 
tagsToRemove.forEach(function saveDraftPage(tagtitle, text) {
afc.talktext = afc.talktext.replace(new RegExp('\\{\\{\\s*' + tag + '\\s*(\\|.*?)?\\}\\}\\n?'), '');
});
 
// TODO: handle edit conflict
var tagsToAddText = tagsToAdd.map(function (tag) {
var editParams = {
return '{{' + tag + '}}';
"action": "edit",
}).join('\n') + (tagsToAdd.length ? '\n' : '');
"title": title,
"text": text,
"summary": msg('editsummary-main')
};
if (ui.captchaLayout && ui.captchaLayout.isElementAttached()) {
editParams.captchaid = afc.captchaid;
editParams.captchaword = ui.captchaInput.getValue();
ui.fieldset.removeItems([ui.captchaLayout]);
}
return afc.api.postWithEditToken(editParams).then(function (data) {
if (!data.edit || data.edit.result !== 'Success') {
if (data.edit && data.edit.captcha) {
// Handle captcha for non-confirmed users
 
var url = data.edit.captcha.url;
afc.talktext = tagsToAddText + (afc.talktext || '');
afc.captchaid = data.edit.captcha.id; // abuse of global?
ui.fieldset.addItems([
ui.captchaLayout = new OO.ui.FieldLayout(ui.captchaInput = new OO.ui.TextInputWidget({
placeholder: msg('captcha-placeholder'),
required: true
}), {
warnings: [ new OO.ui.HtmlSnippet('<img src=' + url + '>') ],
label: msg('captcha-label'),
align: 'top',
help: msg('captcha-helptip'),
helpInline: true,
}),
], /* position */ 6); // just after submit button
// TODO: submit when enter key is pressed in captcha field
 
return $.Deferred().reject('captcha');
 
afc.api.postWithEditToken({
"action": "edit",
"title": new mw.Title(draft).getTalkPage().toText(),
"text": afc.talktext,
"summary": msg('editsummary-talk')
}).then(function (data) {
if (data.edit && data.edit.result === 'Success') {
ui.talkStatusArea.setType('success');
ui.talkStatusArea.setLabel(msg('status-talk-success'));
} else {
return $.Deferred().reject('unexpected-result');
}
}
}).catch(function (code, err) {
});
ui.talkStatusArea.setType('error');
}
ui.talkStatusArea.setLabel(msg('error-saving-talk', makeErrorMessage(code, err)));
});
 
/**
}).catch(function (code, err) {
* @param {Object} page - page information from the API
ui.mainStatusArea.setType('error');
* @returns {string} final draft page text to save
ui.mainStatusArea.setLabel(msg('error-main', makeErrorMessage(code, err)));
*/
ui.submitButton.setDisabled(false);
function prepareDraftText(page) {
var text = page.revisions[0].slots.main.content;
 
var header = '';
 
// Handle short description
var shortDescTemplateExists = /\{\{[Ss]hort ?desc(ription)?\s*\|/.test(text);
var shortDescExists = !!page.description;
var existingShortDesc = page.description;
 
if (ui.shortdescInput.getValue()) {
// 1. No shortdesc - insert the one provided by user
if (!shortDescExists) {
header += '{{Short description|' + ui.shortdescInput.getValue() + '}}\n';
 
// 2. Shortdesc exists from {{short description}} template - replace it
} else if (shortDescExists && shortDescTemplateExists) {
text = text.replace(/\{\{[Ss]hort ?desc(ription)?\s*\|.*?\}\}\n*/g, '');
header += '{{Short description|' + ui.shortdescInput.getValue() + '}}\n';
 
// 3. Shortdesc exists, but not generated by {{short description}}. If the user
// has changed the value, save the new value
} else if (shortDescExists && existingShortDesc !== ui.shortdescInput.getValue()) {
header += '{{Short description|' + ui.shortdescInput.getValue() + '}}\n';
 
// 4. Shortdesc exists, but not generated by {{short description}}, and user hasn't changed the value
} else {
// Do nothing
}
} else {
// User emptied the shortdesc field (or didn't exist from before): remove any existing shortdesc.
// This doesn't remove any shortdesc that is generated by other templates
// Race condition (FIXME): if someone else added a shortdesc to the draft after this user opened the wizard,
// that shortdesc gets removed
text = text.replace(/\{\{[Ss]hort ?desc(ription)?\s*\|.*?\}\}\n*/g, '');
}
 
 
// Draft topics
debug(ui.oresTopicInput);
if (ui.oresTopicLayout.isVisible()) {
afc.oresTopics = ui.oresTopicInput.getValue();
}
if (afc.oresTopics && afc.oresTopics.length) {
text = text.replace(/\{\{[Dd]raft topics\|.*?\}\}\n*/g, '');
header += '{{Draft topics|' + afc.oresTopics.join('|') + '}}\n';
}
 
// Add AfC topic
text = text.replace(/\{\{AfC topic\|(.*?)\}\}/g, '');
header += '{{AfC topic|' + ui.afcTopicInput.getValue() + '}}\n';
 
// put AfC submission template
header += '{{subst:submit|1=' + (mw.util.getParamValue('username') || '{{subst:REVISIONUSER}}') + '}}\n';
 
// insert everything to the top
text = header + text;
debug(text);
 
return text;
}
 
 
/**
* @param {string} initialText - initial talk page text
* @returns {string} - final talk page text to save
*/
function prepareTalkText(initialText) {
var text = initialText;
 
// TODO: this can be improved to put tags within {{WikiProject banner shell}} (if already present or otherwise)
var alreadyExistingWikiProjects = extractWikiProjectTagsFromText(text);
var alreadyExistingTags = alreadyExistingWikiProjects.map(function (e) {
return e.name;
});
var tagsToAdd = ui.talkTagsInput.getValue().filter(function (tag) {
return alreadyExistingTags.indexOf(tag) === -1;
});
var tagsToRemove = alreadyExistingTags.filter(function (tag) {
return ui.talkTagsInput.getValue().indexOf(tag) === -1;
});
 
tagsToRemove.forEach(function (tag) {
text = text.replace(new RegExp('\\{\\{\\s*' + tag + '\\s*(\\|.*?)?\\}\\}\\n?'), '');
});
 
var tagsToAddText = tagsToAdd.map(function (tag) {
return '{{' + tag + '}}';
}).join('\n') + (tagsToAdd.length ? '\n' : '');
 
text = tagsToAddText + (text || '');
 
// remove |class=draft parameter in any WikiProject templates
text = text.replace(/(\{\{wikiproject.*?)\|\s*class\s*=\s*draft\s*/gi, '$1');
 
return text;
}
 
Line 672 ⟶ 809:
 
/**
* Expands wikilinkwikilinks syntaxand intoexternal HTMLlinks <a>into tagsHTML.
* Used instead of mw.msg(...).parse() because we want links to open in a new tab.,
* and we don't want tags to be mangled.
* @param {string} input
* @returns {string}
*/
function linkify(input) {
return input
.replace(
/\[\[:?(?:([^|\]]+?)\|)?([^\]|]+?)\]\]/g,
function(_, target, text) {
if (!target) {
target = text;
}
return '<a target="_blank" href="' + mw.util.getUrl(target) +
'" title="' + target.replace(/"/g, '&#34;') + '">' + text + '</a>';
}
)
return '<a target="_blank" href="' + mw.util.getUrl(target) +
// for ext links, display text should be given
'" title="' + target.replace(/"/g, '&#34;') + '">' + text + '</a>';
.replace(
}
/\[(\S*?) (.*?)\]/g,
);
function (_, target, text) {
return '<a target="_blank" href="' + target + '">' + text + '</a>';
}
);
}
 
Line 696 ⟶ 842:
 
function makeErrorMessage(code, err) {
if (code === 'http') {
return 'http: there is no internet connectivity';
}
return code + (err && err.error && err.error.info ? ': ' + err.error.info : '');
}