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

Content deleted Content added
Create wizard for submitting AFC drafts
 
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
 
(38 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]].
* 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]]
* Licence: MIT
Line 12 ⟶ 16:
/* globals mw, $, OO */
/* <nowiki> */
 
(function () {
 
$.when(
$.ready,
mw.loader.using([
'mediawiki.util', 'mediawiki.api', 'mediawiki.widgetsTitle']),
'mediawiki.widgets', 'oojs-ui-core', 'oojs-ui-widgets'
])
).then(function () {
if (mw.config.get('wgPageName') !== 'Wikipedia:Articles_for_creation/Submitting' ||
 
if ( mw.config.get('wgPageNamewgAction') !== 'Wikipedia:Submitting_your_draft_...view') {
return;
}
init();
});
 
var afc = {}, ui = {};
if (mw.util.getParamValue('withJS') !== 'MediaWiki:AFC-submit-wizard.js') {
window.afc = afc;
return;
afc.ui = ui;
 
var config = {
allowedNamespaces: [2, 118, 5], // User, Draft, WT
debounceDelay: 500,
redirectionDelay: 1000,
defaultAfcTopic: 'other'
};
 
// TODO: move to a separate JSON subpage, would be feasible once [[phab:T198758]] is resolved
var messages = {
"document-title": "Submitting your draft ...",
"page-title": "Submitting your draft ...",
"fieldset-label": "Submit your draft for review at Articles for Creation (AfC)",
"title-label": "Draft title",
"title-placeholder": "Enter the draft page name, usually begins with \"Draft:\"",
"title-helptip": "This should be pre-filled if you clicked the link while on the draft page",
"rawclass-label": "Choose the most appropriate category",
"rawclass-helptip": "For biographies about scholars, choose one of the two biography categories rather than one associated to their field",
"shortdesc-placeholder": "Briefly describe the subject in 2–5 words (eg. \"British astronomer\", \"Cricket stadium in India\")",
"shortdesc-label": "Short description",
"shortdesc-helptip": "Try not to exceed 40 characters",
"talktags-placeholder": "Start typing to search for tags ...",
"talktags-label": "WikiProject classification tags",
"talktags-helptip": "Adding the 1–4 most applicable WikiProjects is plenty. For example, if you add the Physics tag, you do not need to also add the Science tag.",
"orestopic-placeholder": "Start typing to search for topics ...",
"orestopic-label": "Topic classifiers",
"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 help, you can ask at the <b>[[WP:AFCHD|AfC help desk]]</b> or get live help via <b>[[WP:IRCHELP|IRC]]</b> or <b>[[WP:DISCORD|Discord]]</b>.<br>Facing some issues in using this form? <b>[/w/index.php?title=Wikipedia_talk: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.",
"validation-wrongns": "Please check draft title – it should begin with \"Draft:\" or \"User:\"",
"warning-norefs": "This draft doesn't appear to contain any references. Please add references, without which it is likely to be declined. See [[Help:Introduction to referencing with Wiki Markup/2|help on adding references]].",
"status-processing": "Processing ...",
"status-saving": "Saving draft page ...",
"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 ...",
"editsummary-talk": "Adding WikiProject tags using [[WP:AFCSW|AfC-submit-wizard]]",
"status-talk-success": "Successfully added WikiProject tags to talk page",
"error-saving-talk": "An error occurred in editing the talk page ($1).",
"error-main": "An error occurred ($1). Please try again or refer to the help desk."
};
 
function init() {
for (var key in messages) {
mw.messages.set('afcsw-' + key, messages[key]);
}
 
document.title = msg('document-title');
var api = new mw.Api({
$('#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 = {
parameters: {
format: 'json',
Line 36 ⟶ 114:
}
}
});
 
// Two different API objects so that aborts on the lookupApi don't stop the final
var draftLayout, talkTagsLayout, shortdescLayout, isBLPLayout, isCompanyLayout, topicsLayout, submitLayout, draftInput, talkTagsInput, shortdescInput, isBLPInput, isCompanyInput, topicsInput, submitButton;
// evaluate process
afc.api = new mw.Api(apiOptions);
afc.lookupApi = new mw.Api(apiOptions);
 
constructUI();
}
 
function constructUI() {
// global
var pagetext, oresTopics;
 
ui.fieldset = new OO.ui.FieldsetLayout({
// Create the UI
label: msg('fieldset-label'),
var fieldset = new OO.ui.FieldsetLayout({
classes: [ 'container' ],
label: 'Submit your draft for review at Articles for Creation (AfC)',
classes: ['container'],
items: [
draftLayoutui.titleLayout = new OO.ui.FieldLayout(draftInputui.titleInput = new OOmw.uiwidgets.TextInputWidgetTitleInputWidget({
value: (mw.util.getParamValue('draftpage') || '').replace(/_/g, ' '),
placeholder: msg('Enter the draft title (begins with "Draft:")-placeholder'),
}), {
label: msg('Draft title-label'),
align: 'top',
help: msg('title-helptip'),
help: 'This should be pre-filled if you clicked the link while on the draft page',
helpInline: true
}),
 
talkTagsLayoutui.afcTopicLayout = new OO.ui.FieldLayout(talkTagsInputui.afcTopicInput = new OO.ui.MenuTagMultiselectWidgetRadioSelectInputWidget(), {
label: msg('rawclass-label'),
placeholder: 'Start typing to search for tags ...',
help: msg('rawclass-helptip'),
tagLimit: 10,
autocompletealign: false'inline',
}), {
label: 'Enter WikiProject tags',
align: 'top',
help: 'Adding the 1–4 most pertinent WikiProjects is plenty. For example, if you add the Physics tag, you do not need to also add the Science tag.',
helpInline: true
}),
 
ui.shortdescLayout = new OO.ui.FieldLayout(ui.shortdescInput = new OO.ui.TextInputWidget({
placeholder: msg('shortdesc-placeholder'),
placeholder: 'Briefly describe the subject (eg. "Kenyan astronomer", "Indian dessert")'
maxLength: 100
}), {
label: msg('Short descriptionshortdesc-label'),
align: 'top',
help: msg('Try not to exceed 40 charactersshortdesc-helptip'),
helpInline: true,
}),
 
isBLPLayoutui.talkTagsLayout = new OO.ui.FieldLayout(isBLPInputui.talkTagsInput = new OO.ui.CheckboxInputWidgetMenuTagMultiselectWidget({
placeholder: msg('talktags-placeholder'),
selected: false
tagLimit: 10,
autocomplete: false,
$overlay: $('<div>').addClass('projectTagOverlay').css({
'position': 'absolute',
'z-index': '110'
}).appendTo('body')
}), {
label: msg('This draft is a biography of a living persontalktags-label'),
align: 'inlinetop',
help: msg('talktags-helptip'),
helpInline: true,
}),
 
// This is shown only if the ORES topic lookup fails, or is inconclusive
isCompanyLayout = new OO.ui.FieldLayout(isCompanyInput = new OO.ui.CheckboxInputWidget({
ui.oresTopicLayout = new OO.ui.FieldLayout(ui.oresTopicInput = new OO.ui.MenuTagMultiselectWidget({
selected: false
placeholder: msg('orestopic-placeholder'),
tagLimit: 10,
autocomplete: false, // XXX: doesn't seem to work
options: [ "biography", "women", "food-and-drink", "internet-culture", "linguistics", "literature", "books", "entertainment", "films", "media", "music", "radio", "software", "television", "video-games", "performing-arts", "philosophy-and-religion", "sports", "architecture", "comics-and-anime", "fashion", "visual-arts", "geographical", "africa", "central-africa", "eastern-africa", "northern-africa", "southern-africa", "western-africa", "central-america", "north-america", "south-america", "asia", "central-asia", "east-asia", "north-asia", "south-asia", "southeast-asia", "west-asia", "eastern-europe", "europe", "northern-europe", "southern-europe", "western-europe", "oceania", "business-and-economics", "education", "history", "military-and-warfare", "politics-and-government", "society", "transportation", "biology", "chemistry", "computing", "earth-and-environment", "engineering", "libraries-and-information", "mathematics", "medicine-and-health", "physics", "stem", "space", "technology" ].map(function (e) {
return {
data: e,
label: e
};
})
}), {
label: msg('This draft is about a company or a corporationorestopic-label'),
align: 'inlinetop',
help: msg('orestopic-helptip'),
helpInline: true
}),
 
ui.submitLayout = new OO.ui.FieldLayout(ui.submitButton = new OO.ui.ButtonWidget({
label: msg('Submitsubmit-label'),
flags: [ 'progressive', 'primary' ],
}))
 
]
});
 
ui.footerLayout = new OO.ui.FieldLayout(new OO.ui.LabelWidget({
$('.mw-ui-button').parent().replaceWith(fieldset.$element);
label: $('<div>')
.append(linkify(msg('footer-text')))
}), {
align: 'top'
});
 
afc.topicOptionsLoaded = getJSONPage('Wikipedia:WikiProject Articles for creation/AfC topic map.json').then(function (optionsJson) {
var options = [];
$.each(optionsJson, function (code, info) {
options.push({
label: info.label,
data: code
});
});
ui.afcTopicInput.setOptions(options);
ui.afcTopicInput.setValue(config.defaultAfcTopic);
 
// resolve promise with allowed option codes:
return options.map(function (op) {
return op.data;
});
});
 
ui.oresTopicLayout.toggle(false);
 
var asUser = mw.util.getParamValue('username');
if (asUser && asUser !== mw.config.get('wgUserName')) {
ui.fieldset.addItems([
new OO.ui.FieldLayout(new OO.ui.MessageWidget({
type: 'notice',
inline: true,
label: msg('submitting-as', asUser)
}))
], /* position */ 5); // just before submit button
}
 
// Attach
$('#afc-submit-wizard-container').empty().append(ui.fieldset.$element, ui.footerLayout.$element);
mw.track('counter.gadget_afcsw.opened');
 
// populatePopulate talk page tags for multi-select widget
afc.talkTagOptionsLoaded = getJSONPage('Wikipedia:WikiProject Articles for creation/WikiProject templates.json').then(function (data) {
$.get('https://en.wikipedia.org/w/index.php?title=' + encodeURIComponent("User:Theo's Little Bot/afchwikiproject.js") + '&action=raw&ctype=text/json').then(function (data) {
ui.talkTagsInput.addOptions(Object.keys(data).map(function (k) {
data = JSON.parse(data);
talkTagsInput.addOptions(Object.keys(data).map(function (k) {
return {
data: data[k],
Line 114 ⟶ 248:
});
 
ui.clearTalkTags = function () {
submitButton.$element.on('click', evaluate);
afc.talkTagOptionsLoaded.then(function () {
draftInput.on('change', onDraftInputChange);
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', handleSubmit);
ui.titleInput.on('change', mw.util.debounce(config.debounceDelay, onDraftInputChange));
 
if (mw.util.getParamValue('draftpage')) {
onDraftInputChange();
}
 
// The default font size in monobook and modern are too small at 10px
function onDraftInputChange() {
mw.util.addCSS('.skin-modern .projectTagOverlay, .skin-monobook .projectTagOverlay { font-size: 130%; }');
console.log('draft input changed: "' + draftInput.getValue() + '"');
draftLayout.setErrors([]); // clear old errors
draftLayout.setWarnings([]); // clear old errors
oresTopics = null;
 
afc.beforeUnload = function (e) {
api.get({
e.preventDefault();
"action": "query",
e.returnValue = '';
"prop": "revisions|description|info",
return '';
"titles": draftInput.getValue(),
};
"rvprop": "content",
$(window).on('beforeunload', afc.beforeUnload);
"rvslots": "main",
}
"inprop": "talkid"
}).then(function (json) {
var page = json.query.pages[0];
var preNormalizedTitle = json.query.normalized && json.query.normalized[0] &&
json.query.normalized[0].from;
console.log('draftInput.value: "' + draftInput.getValue() + '"');
console.log('preNormalizedTitle: "' + preNormalizedTitle + '"');
console.log('page.title: "' + page.title + '"');
if (draftInput.getValue() !== (preNormalizedTitle || page.title)) {
return; // user must have changed the title already
}
 
function onDraftInputChange() {
if (!page || page.missing) {
afc.lookupApi.abort(); // abort older API requests
draftLayout.setErrors([
'Please check draft title. No such draft exists.'
]);
return;
}
pagetext = page.revisions[0].slots.main.content;
 
var drafttitle = ui.titleInput.getValue().trim();
// Show no refs warning
if (!drafttitle) { // empty
if (!/<ref>/.test(pagetext) && !/\{\{[Ss]fn\}\}/.test(pagetext)) {
return;
draftLayout.setWarnings([
}
new OO.ui.HtmlSnippet('Your draft doesn\'t seem to contain any references. Please add references, without this it will almost certainly be declined. See <a href="/wiki/Help:Introduction_to_referencing_with_Wiki_Markup/2" target="_blank">help on adding references</a>.')
debug('draft input changed: "' + ui.titleInput.getValue() + '"');
]);
 
// re-initialize
ui.titleLayout.setErrors([]);
ui.titleLayout.setWarnings([]);
afc.oresTopics = null;
afc.talktext = null;
afc.pagetext = null;
ui.clearTalkTags();
 
afc.lookupApi.get({
"action": "query",
"prop": "revisions|description|info",
"titles": drafttitle,
"rvprop": "content",
"rvslots": "main"
}).then(setPrefillsFromPageData);
 
var titleObj = mw.Title.newFromText(drafttitle);
if (!titleObj || titleObj.isTalkPage()) {
return;
}
var talkpagename = titleObj.getTalkPage().toText();
afc.lookupApi.get({
"action": "query",
"prop": "revisions",
"titles": talkpagename,
"rvprop": "content",
"rvslots": "main",
}).then(setPrefillsFromTalkPageData);
 
}
 
function setPrefillsFromPageData(json) {
debug('page fetch query', json);
var page = json.query.pages[0];
var preNormalizedTitle = json.query.normalized && json.query.normalized[0] &&
json.query.normalized[0].from;
debug('page.title: "' + page.title + '"');
if (ui.titleInput.getValue() !== (preNormalizedTitle || page.title)) {
return; // user must have changed the title already
}
var errors = errorsFromPageData(page);
if (errors.length) {
ui.titleLayout.setErrors(errors);
return;
}
ui.titleLayout.setWarnings(warningsFromPageData(page));
 
afc.pagetext = page.revisions[0].slots.main.content;
 
// Set AfC topic category
var topicMatch = afc.pagetext.match(/\{\{AfC topic\|(.*?)\}\}/);
if (topicMatch) {
afc.topicOptionsLoaded.then(function(allowedCodes) {
var topic = topicMatch[1];
debug("Allowed topic codes fetched:", allowedCodes);
debug("AfC topic found:", topic);
// if the code found in the template is an invalid one, keep the default to "other",
// rather than the first item in the list
if (allowedCodes.indexOf(topic) !== -1) {
ui.afcTopicInput.setValue(topic);
} else {
ui.afcTopicInput.setValue(config.defaultAfcTopic);
}
});
} else {
ui.afcTopicInput.setValue(config.defaultAfcTopic);
}
 
// setSet shortdescshort description in form
ui.shortdescInput.setValue(page.description || '');
 
// Guess WikiProject tags from infoboxes on the page
// fetch talk page to get wikiproject tags
afc.ibxmapLoaded.then(function (ibxmap) {
if (page.talkid) {
var infoboxRgx = /\{\{([Ii]nfobox [^|}]*)/g,
fetchTalkPageAndPopulateTags(page.talkid);
wikiprojects = [],
match;
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]);
}
}
debug('wikiprojects from infobox: ', wikiprojects);
ui.addTalkTags(wikiprojects);
});
 
// fillFill ORES topics
getOresTopics(page.lastrevid).then(function (topics) {
console.logdebug('ORES topics: ', topics);
if (!topics || !topics.length) { // unexpected API response or API returns unsorted
ui.oresTopicLayout.toggle(true);
if (!topicsLayout || !topicsLayout.isElementAttached()) {
} else {
createOresTopicSelectionMenu();
ui.oresTopicLayout.toggle(false);
}
afc.oresTopics = topics;
} else {
}
if (topicsLayout && topicsLayout.isElementAttached()) {
}, function () {
fieldset.removeItems([topicsLayout]);
ui.oresTopicLayout.toggle(true);
}
});
oresTopics = topics;
}
}
}, createOresTopicSelectionMenu);
 
function setPrefillsFromTalkPageData (json) {
});
var talkpage = json.query.pages[0];
if (!talkpage || talkpage.missing) {
return;
}
afc.talktext = talkpage.revisions[0].slots.main.content;
debug(afc.talktext);
 
var existingWikiProjects = extractWikiProjectTagsFromText(afc.talktext);
function getOresTopics(revid) {
var existingTags = existingWikiProjects.map(function (e) {
return $.get('https://ores.wikimedia.org/v3/scores/enwiki/?models=drafttopic&revids=' + revid).then(function (json) {
return e.name;
});
debug(existingTags);
ui.addTalkTags(existingTags);
}
 
/**
// null is returned if at any point something in the API output is unexpected
* @param {Object} page - from query API response
return json &&
* @returns {string[]}
json.enwiki.scores &&
*/
json.enwiki.scores[revid] &&
function errorsFromPageData(page) {
json.enwiki.scores[revid].drafttopic &&
if (!page || page.invalid) {
json.enwiki.scores[revid].drafttopic.score &&
return [msg('validation-invalidtitle')];
(json.enwiki.scores[revid].drafttopic.score.prediction instanceof Array) &&
}
json.enwiki.scores[revid].drafttopic.score.prediction.map(function (topic, idx, topics) {
if (page.missing) {
// Remove Asia.Asia* if Asia.South-Asia is present (example)
return [msg('validation-missingtitle')];
if (topic.slice(-1) === '*') {
}
let metatopic = topic.split('.').slice(0, -1).join('.');
if (config.allowedNamespaces.indexOf(page.ns) === -1) {
for (let i = 0; i < topics.length; i++) {
return [msg('validation-wrongns')];
if (topics[i] !== topic && topics[i].startsWith(metatopic)) {
}
return;
return [];
}
}
 
/**
* @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');
}
 
// 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) {
 
// null is returned if at any point something in the API output is unexpected
// ES2020 has optional chaining, but of course on MediaWiki we're still stuck with ES5
return json &&
json.enwiki &&
json.enwiki.scores &&
json.enwiki.scores[revid] &&
json.enwiki.scores[revid].drafttopic &&
json.enwiki.scores[revid].drafttopic.score &&
(json.enwiki.scores[revid].drafttopic.score.prediction instanceof Array) &&
json.enwiki.scores[revid].drafttopic.score.prediction.map(function (topic, idx, topics) {
// Remove Asia.Asia* if Asia.South-Asia is present (example)
if (topic.slice(-1) === '*') {
var metatopic = topic.split('.').slice(0, -1).join('.');
for (var i = 0; i < topics.length; i++) {
if (topics[i] !== topic && topics[i].startsWith(metatopic)) {
return;
}
return metatopic.split('.').pop();
}
return topicmetatopic.split('.').pop();
})
return topic.filtersplit(function '.').pop(e) {;
})
return e; // filter out undefined from above
.filter(function (e) {
})
return e; // filter out undefined from above
.map(function (topic) {
})
// convert topic string to normalised form
return.map(function (topic) {
// convert topic string to normalised form
.replace(/[A-Z]/g, function (match) {
return match[0].toLowerCase();topic
.replace(/[A-Z]/g, function (match) {
})
return match[0].toLowerCase();
.replace(/ /g, '-')
})
.replace(/& /g, 'and-');
} .replace(/&/g, 'and');
});
});
}
 
/***
* @param {string} text
* @returns {{wikitext: string, name: string}[]}
*/
function extractWikiProjectTagsFromText(text) {
if (!text) {
return [];
}
 
// this is best-effort, no guaranteed accuracy
var existingTags = [];
var rgx = /\{\{(WikiProject [^|}]*).*?\}\}/g;
var match;
while (match = rgx.exec(text)) { // jshint ignore:line
var tag = match[1].trim();
if (tag === 'WikiProject banner shell') {
continue;
}
existingTags.push({
wikitext: match[0],
name: tag
});
}
return existingTags;
}
 
/**
function createOresTopicSelectionMenu() {
* @param {string} type
// create menu for user to make the selections
* @param {string} message
fieldset.addItems([
*/
topicsLayout = new OO.ui.FieldLayout(topicsInput = new OO.ui.MenuTagMultiselectWidget({
function setMainStatus(type, message) {
placeholder: 'Start typing to search for tags ...',
if (!ui.mainStatusLayout || !ui.mainStatusLayout.isElementAttached()) {
tagLimit: 10,
ui.fieldset.addItems([
autocomplete: false, // XXX: doesn't seem to work
ui.mainStatusLayout = new OO.ui.FieldLayout(ui.mainStatusArea = new OO.ui.MessageWidget())
options: ["biography", "women", "food-and-drink", "internet-culture", "linguistics", "literature", "books", "entertainment", "films", "media", "music", "radio", "software", "television", "video-games", "performing-arts", "philosophy-and-religion", "sports", "architecture", "comics-and-anime", "fashion", "visual-arts", "geographical", "africa", "central-africa", "eastern-africa", "northern-africa", "southern-africa", "western-africa", "central-america", "north-america", "south-america", "asia", "central-asia", "east-asia", "north-asia", "south-asia", "southeast-asia", "west-asia", "eastern-europe", "europe", "northern-europe", "southern-europe", "western-europe", "oceania", "business-and-economics", "education", "history", "military-and-warfare", "politics-and-government", "society", "transportation", "biology", "chemistry", "computing", "earth-and-environment", "engineering", "libraries-and-information", "mathematics", "medicine-and-health", "physics", "stem", "space", "technology"].map(function (e) {
]);
return {
data: e,
label: e
};
})
}), {
label: 'Pick draft topics',
align: 'top',
help: 'Pick the suitable items',
helpInline: true
})
], /* insertion point */ 5);
}
ui.mainStatusArea.setType(type);
ui.mainStatusArea.setLabel(message);
}
 
function fetchTalkPageAndPopulateTags(talkid) {
api.get({
"action": "query",
"prop": "revisions",
"pageids": talkid,
"rvprop": "content",
"rvslots": "main",
}).then(function (json) {
var talkpage = json.query.pages[0];
if (!talkpage || talkpage.missing) {
return;
}
var talktext = talkpage.revisions[0].slots.main.content;
console.log(talktext);
 
/**
// this is best-effort, no guaranteed accuracy
* @param {string} type
var existingTags = [];
* @param {string} message
var rgx = /\{\{(WikiProject [^|}]*)/g;
*/
var match;
function setTalkStatus(type, message) {
while (match = rgx.exec(talktext)) { // jshint ignore:line
if (!ui.talkStatusLayout) {
var tag = match[1].trim();
ui.fieldset.addItems([
if (tag === 'WikiProject banner shell') {
ui.talkStatusLayout = new OO.ui.FieldLayout(ui.talkStatusArea = new OO.ui.MessageWidget())
continue;
}]);
}
existingTags.push(tag);
ui.talkStatusArea.setType(type);
}
ui.talkStatusArea.setLabel(message);
talkTagsInput.setValue(existingTags);
}
console.log(existingTags);
 
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);
 
afc.api.get({
function evaluate() {
"action": "query",
var draft = mw.Title.newFromText(draftInput.getValue());
"prop": "revisions|description",
console.log(draft);
"titles": draft,
console.log(isBLPInput.getValue());
"rvprop": "content",
if (!draft) {
"rvslots": "main",
draftLayout.setErrors([
}).then(function (json) {
'Please check draft title. Invalid title provided'
var apiPage = json.query.pages[0];
]);
 
var errors = errorsFromPageData(apiPage);
if (errors.length) {
ui.titleLayout.setErrors(errors);
ui.fieldset.removeItems([ui.mainStatusLayout]);
ui.submitButton.setDisabled(false);
ui.titleLayout.scrollElementIntoView();
return;
}
 
var text = prepareDraftText(apiPage);
api.get({
"action": "query",
"prop": "revisions",
"titles": draft.toText(),
"rvprop": "content|timestamp",
"rvslots": "main",
"curtimestamp": 1
}).then(function (json) {
var page = json.query.pages[0];
if (!page || page.invalid || page.missing) {
draftLayout.setErrors([
'Please check draft title. No such draft exists.'
]);
return;
}
var text = page.revisions[0].slots.main.content;
 
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);
// add shortdesc
setTimeout(function () {
if (shortdescInput.getValue()) {
___location.href = mw.util.getUrl(draft);
text = text.replace(/\{\{[Ss]hort description\|.*?\}\}\n*/g, '');
}, config.redirectionDelay);
header += '{{Short description|' + shortdescInput.getValue() + '}}\n';
}, function (code, err) {
if (code === 'captcha') {
ui.fieldset.removeItems([ui.mainStatusLayout, ui.talkStatusLayout]);
ui.captchaLayout.scrollElementIntoView();
mw.track('counter.gadget_afcsw.submit_captcha');
} else {
setMainStatus('error', msg('error-saving-main', makeErrorMessage(code, err)));
mw.track('counter.gadget_afcsw.submit_failed');
mw.track('counter.gadget_afcsw.submit_failed_' + code);
}
ui.submitButton.setDisabled(false);
});
 
var talktext = prepareTalkText(afc.talktext);
if (!afc.talktext && !talktext) {
// No content earlier, no content now. Stop here to avoid
// creating the talk page as empty.
return;
}
 
setTalkStatus('notice', msg('status-saving-talk'));
// draft topics
afc.api.postWithEditToken({
console.log(topicsInput);
"action": "edit",
if (topicsInput && topicsInput.isElementAttached()) {
"title": new mw.Title(draft).getTalkPage().toText(),
oresTopics = topicsInput.getValue();
"text": talktext,
}
"summary": msg('editsummary-talk')
if (oresTopics.length) {
}).then(function (data) {
text = text.replace(/\{\{[Dd]raft topics\|.*?\}\}\n*/g, '');
if (data.edit && data.edit.result === 'Success') {
header += '{{Draft topics|' + oresTopics.join('|') + '}}\n';
setTalkStatus('success', msg('status-talk-success'));
} else {
return $.Deferred().reject('unexpected result');
}
}).catch(function (code, err) {
setTalkStatus('error', msg('error-saving-talk', makeErrorMessage(code, err)));
});
 
 
}).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);
});
 
}
 
function saveDraftPage(title, text) {
 
// TODO: handle edit conflict
var editParams = {
"action": "edit",
"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.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');
 
} else {
// put BLP/Company categories
return $.Deferred().reject('unexpected-result');
text = text.replace(/\{\{[Dd]raft(BLP|Corp)\}\}\n*/g, '');
if (isBLPInput.isSelected()) {
header += '{{DraftBLP}}\n';
}
if (isCompanyInput.isSelected()) {
header += '{{DraftCorp}}\n';
}
}
});
}
 
/**
// insert shortdesc, draft topics, blp/corp templates at the top
* @param {Object} page - page information from the API
text = header + text;
* @returns {string} final draft page text to save
*/
function prepareDraftText(page) {
var text = page.revisions[0].slots.main.content;
 
var header = '';
// put AFC submission template (bottom)
text += '\n\n' + '{{AFC submission|||ts=' + makeMWTimestamp(new Date()) + '|u=' + mw.config.get('wgUserName') + '|ns=' + mw.config.get('wgNamespaceNumber') + '}}';
 
// Handle short description
console.log(text);
var shortDescTemplateExists = /\{\{[Ss]hort ?desc(ription)?\s*\|/.test(text);
api.postWithEditToken({
var shortDescExists = !!page.description;
"action": "edit",
var existingShortDesc = page.description;
"title": draft.toText(),
"text": text,
"summary": 'Submitting ([[MediaWiki:AFC-submit-wizard.js|assisted]])'
}).then(function (data) {
if (data.edit && data.edit.result === 'Success') {
submitButton.setSuccessMessages([
'Submission succeeded. Redirecting you to the draft page ...'
]);
setTimeout(function () {
___location.href = mw.util.getUrl(draft.toText());
}, 1000);
} else {
return $.Deferred().reject('unexpected-result');
}
}).catch(function (err) {
submitButton.setErrors([
'An error occurred (' + err + '). Please try again or refer to the help desk.'
]);
});
 
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, '');
}
 
function makeMWTimestamp(date) {
var pad = function (num) {
return ('0' + String(num)).slice(-2);
};
var Y = date.getUTCFullYear(),
M = date.getUTCMonth() + 1,
D = date.getUTCDate(),
H = date.getUTCHours(),
m = date.getUTCMinutes(),
s = date.getUTCSeconds();
 
// Draft topics
return Y + pad(M) + pad(D) + pad(H) + pad(m) + pad(s);
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;
}
 
/**
* Load a JSON page from the wiki.
* Use API (instead of $.getJSON with action=raw) to take advantage of caching
* @param {string} page
* @returns {jQuery.Promise<Record<string, any>>}
**/
function getJSONPage (page) {
return afc.api.get({
action: 'query',
titles: page,
prop: 'revisions',
rvprop: 'content',
rvlimit: 1,
rvslots: 'main',
uselang: 'content',
maxage: '3600', // 1 hour
smaxage: '3600',
formatversion: 2
}).then(function (json) {
var content = json.query.pages[0].revisions[0].slots.main.content;
return JSON.parse(content);
}).catch(function (code, err) {
console.error(makeErrorMessage(code, err));
});
}
 
/**
* Expands wikilinks and external links into HTML.
* 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>';
}
)
// for ext links, display text should be given
.replace(
/\[(\S*?) (.*?)\]/g,
function (_, target, text) {
return '<a target="_blank" href="' + target + '">' + text + '</a>';
}
);
}
 
function msg(key) {
var messageArgs = Array.prototype.slice.call(arguments, 1);
return mw.msg.apply(mw, ['afcsw-' + key].concat(messageArgs));
}
 
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 : '');
}
 
function debug() {
Array.prototype.slice.call(arguments).forEach(function (arg) {
console.log(arg);
});
}
 
})(); // File-level closure to protect functions from being exposed to the global scope or overwritten
 
/* </nowiki> */