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

Content deleted Content added
update
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
 
(37 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 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, mainStatusLayout, mainStatusArea, talkStatusLayout, talkStatusArea;
// evaluate process
afc.api = new mw.Api(apiOptions);
afc.lookupApi = new mw.Api(apiOptions);
 
constructUI();
}
 
function constructUI() {
// global
var pagetext, talktext, 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) {
// populate talk page tags for multi-select widget
var options = [];
$.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) {
$.each(optionsJson, function (code, info) {
data = JSON.parse(data);
options.push({
talkTagsInput.addOptions(Object.keys(data).map(function (k) {
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');
 
// Populate talk page tags for multi-select widget
afc.talkTagOptionsLoaded = getJSONPage('Wikipedia:WikiProject Articles for creation/WikiProject templates.json').then(function (data) {
ui.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%; }');
if (!draftInput.getValue().trim()) { // empty
return;
}
console.log('draft input changed: "' + draftInput.getValue() + '"');
 
afc.beforeUnload = function (e) {
// re-initialize
e.preventDefault();
draftLayout.setErrors([]);
e.returnValue = '';
draftLayout.setWarnings([]);
return '';
oresTopics = null;
};
talktext = null;
$(window).on('beforeunload', afc.beforeUnload);
pagetext = null;
}
 
function onDraftInputChange() {
api.get({
afc.lookupApi.abort(); // abort older API requests
"action": "query",
"prop": "revisions|description|info",
"titles": draftInput.getValue(),
"rvprop": "content",
"rvslots": "main",
"inprop": "talkid"
}).then(function (json) {
console.log(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
}
if (!page || page.invalid) {
draftLayout.setErrors([
'Please check draft title. This title is invalid.'
]);
return;
}
if (page.missing) {
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;
}
}
}, function() {
if (!topicsLayout || !topicsLayout.isElementAttached()) {
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);
}
 
/**
* @param {Object} page - from query API response
* @returns {string[]}
*/
function errorsFromPageData(page) {
if (!page || page.invalid) {
return [msg('validation-invalidtitle')];
}
if (page.missing) {
return [msg('validation-missingtitle')];
}
if (config.allowedNamespaces.indexOf(page.ns) === -1) {
return [msg('validation-wrongns')];
}
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 &&
return 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) {&&
(json.enwiki.scores[revid].drafttopic.score.prediction instanceof Array) &&
// Remove Asia.Asia* if Asia.South-Asia is present (example)
json.enwiki.scores[revid].drafttopic.score.prediction.map(function (topic, idx, topics) {
if (topic.slice(-1) === '*') {
// Remove Asia.Asia* if Asia.South-Asia is present (example)
var metatopic = topic.split('.').slice(0, -1).join('.');
forif (var itopic.slice(-1) === 0; i < topics.length; i++'*') {
var metatopic = topic.split('.').slice(0, -1).join('.');
if (topics[i] !== topic && topics[i].startsWith(metatopic)) {
for (var i = 0; i < topics.length; i++) {
return;
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;
}
talktext = talkpage.revisions[0].slots.main.content;
console.log(talktext);
 
/**
var existingTags = extractWikiProjectTagsFromText(talktext);
* @param {string} type
talkTagsInput.setValue(existingTags.map(function(e) {
* @param {string} message
return e.name;
*/
}));
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() {
console.log(existingTags);
 
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 extractWikiProjectTagsFromText(text) {
"action": "query",
if (!text) {
"prop": "revisions|description",
return [];
"titles": draft,
"rvprop": "content",
"rvslots": "main",
}).then(function (json) {
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);
// this is best-effort, no guaranteed accuracy
 
var existingTags = [];
setMainStatus('notice', msg('status-saving'));
var rgx = /\{\{(WikiProject [^|}]*).*?\}\}/g;
saveDraftPage(draft, text).then(function () {
var match;
setMainStatus('success', msg('status-redirecting'));
while (match = rgx.exec(text)) { // jshint ignore:line
mw.track('counter.gadget_afcsw.submit_succeeded');
var tag = match[1].trim();
 
if (tag === 'WikiProject banner shell') {
$(window).off('beforeunload', afc.beforeUnload);
continue;
setTimeout(function () {
___location.href = mw.util.getUrl(draft);
}, config.redirectionDelay);
}, 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);
existingTags.push({ wikitext: match[0], name: tag });
});
 
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;
}
return existingTags;
}
 
setTalkStatus('notice', msg('status-saving-talk'));
function evaluate() {
afc.api.postWithEditToken({
"action": "edit",
"title": new mw.Title(draft).getTalkPage().toText(),
"text": talktext,
"summary": msg('editsummary-talk')
}).then(function (data) {
if (data.edit && data.edit.result === 'Success') {
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)));
});
 
fieldset.addItems([
mainStatusLayout = new OO.ui.FieldLayout(mainStatusArea = new OO.ui.MessageWidget())
]);
 
}).catch(function (code, err) {
mainStatusArea.setType('notice');
setMainStatus('error', msg('error-main', makeErrorMessage(code, err)));
mainStatusArea.setLabel('Processing ...');
ui.submitButton.setDisabled(false);
mw.track('counter.gadget_afcsw.submit_failed');
mw.track('counter.gadget_afcsw.submit_failed_' + code);
});
 
}
var draft = draftInput.getValue();
console.log(draft);
console.log(isBLPInput.getValue());
 
function saveDraftPage(title, text) {
api.get({
"action": "query",
"prop": "revisions",
"titles": draft,
"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.'
]);
mainStatusArea.setType('notice');
mainStatusArea.setLabel(''); // XXX
return;
}
var text = page.revisions[0].slots.main.content;
 
// TODO: handle edit conflict
var header = '';
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;
// add shortdesc
afc.captchaid = data.edit.captcha.id; // abuse of global?
if (shortdescInput.getValue()) {
ui.fieldset.addItems([
text = text.replace(/\{\{[Ss]hort description\|.*?\}\}\n*/g, '');
ui.captchaLayout = new OO.ui.FieldLayout(ui.captchaInput = new OO.ui.TextInputWidget({
header += '{{Short description|' + shortdescInput.getValue() + '}}\n';
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');
// draft topics
console.log(topicsInput);
if (topicsInput && topicsInput.isElementAttached()) {
oresTopics = topicsInput.getValue();
}
if (oresTopics.length) {
text = text.replace(/\{\{[Dd]raft topics\|.*?\}\}\n*/g, '');
header += '{{Draft topics|' + oresTopics.join('|') + '}}\n';
}
 
} 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{{subst:submit|' + (mw.util.getParamValue('username') || mw.config.get('wgUserName')) + '}}';
 
// Handle short description
console.log(text);
var shortDescTemplateExists = /\{\{[Ss]hort ?desc(ription)?\s*\|/.test(text);
var shortDescExists = !!page.description;
var existingShortDesc = page.description;
 
if (ui.shortdescInput.getValue()) {
mainStatusArea.setType('notice');
// 1. No shortdesc - insert the one provided by user
mainStatusArea.setLabel('Saving draft page ...');
if (!shortDescExists) {
header += '{{Short description|' + ui.shortdescInput.getValue() + '}}\n';
 
// 2. Shortdesc exists from {{short description}} template - replace it
// saving draft page
} else if (shortDescExists && shortDescTemplateExists) {
api.postWithEditToken({
text = text.replace(/\{\{[Ss]hort ?desc(ription)?\s*\|.*?\}\}\n*/g, '');
"action": "edit",
header += '{{Short description|' + ui.shortdescInput.getValue() + '}}\n';
"title": draft,
"text": text,
"summary": 'Submitting ([[MediaWiki:AFC-submit-wizard.js|assisted]])'
}).then(function (data) {
if (data.edit && data.edit.result === 'Success') {
mainStatusArea.setType('success');
mainStatusArea.setLabel('Submission succeeded. Redirecting you to the draft page ...');
 
// 3. Shortdesc exists, but not generated by {{short description}}. If the user
setTimeout(function () {
// has changed the value, save the new value
___location.href = mw.util.getUrl(draft);
} else if (shortDescExists && existingShortDesc !== ui.shortdescInput.getValue()) {
}, 1000);
header += '{{Short description|' + ui.shortdescInput.getValue() + '}}\n';
} else {
return $.Deferred().reject('unexpected-result');
}
}).catch(function (err) {
mainStatusArea.setType('error');
mainStatusArea.setLabel('An error occurred (' + err + '). Please try again or refer to the help desk.');
});
 
// 4. Shortdesc exists, but not generated by {{short description}}, and user hasn't changed the value
fieldset.addItems([
} else {
talkStatusLayout = new OO.ui.FieldLayout(talkStatusArea = new OO.ui.MessageWidget())
// 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, '');
}
 
talkStatusArea.setType('notice');
talkStatusArea.setLabel('Saving draft talk page ...');
 
// 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
// Process text of the talk page
text = text.replace(/\{\{AfC topic\|(.*?)\}\}/g, '');
var alreadyExistingWikiProjects = extractWikiProjectTagsFromText(talktext);
header += '{{AfC topic|' + ui.afcTopicInput.getValue() + '}}\n';
var alreadyExistingTags = alreadyExistingWikiProjects.map(function(e) {
return e.name;
});
var tagsToAdd = talkTagsInput.getValue().filter(function(tag) {
return alreadyExistingTags.indexOf(tag) === -1;
});
var tagsToRemove = alreadyExistingTags.filter(function(tag) {
return talkTagsInput.getValue().indexOf(tag) === -1;
});
 
// put AfC submission template
tagsToRemove.forEach(function(tag) {
header += '{{subst:submit|1=' + (mw.util.getParamValue('username') || '{{subst:REVISIONUSER}}') + '}}\n';
talktext = talktext.replace(new RegExp('\\{\\{\\s*' + tag + '\\s*(\\|.*?)?\\}\\}\\n?'), '');
});
 
// insert everything to the top
var tagsToAddText = tagsToAdd.map(function (tag) {
text = header + text;
return '{{' + tag + '}}';
debug(text);
}).join('\n') + (tagsToAdd.length ? '\n' : '');
 
return text;
talktext = tagsToAddText + (talktext || '');
}
 
api.postWithEditToken({
"action": "edit",
"title": new mw.Title(draft).getTalkPage().toText(),
"text": talktext,
"summary": 'Adding WikiProject tags ([[MediaWiki:AFC-submit-wizard.js|assisted]])'
}).then(function(data) {
if (data.edit && data.edit.result === 'Success') {
talkStatusArea.setType('success');
talkStatusArea.setLabel('Successfully added WikiProject tags to talk page');
} else {
return $.Deferred().reject('unexpected-result');
}
}).catch(function (err) {
talkStatusArea.setType('error');
talkStatusArea.setLabel('An error occurred in editing the talk page (' + err + ').');
});
 
/**
* @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> */