User:The Photographer/QICvote.js

mw.loader.using('oojs-ui-core').done(async function() {
"use strict";
if (!(mw.config.get("wgAction") === "view" && (mw.config.get("wgPageName") === "Commons:Quality_images_candidates/candidate_list" || mw.config.get("wgPageName") === "Commons:Quality_images_candidates"))) return;
mw.loader.load('/w/index.php?title=MediaWiki:Gadget-QICvote.css&action=raw&ctype=text/css','text/css');
const CONFIRM_BAR = [
    "<div class='loading_overly'>",
      "<div><img src='https://upload.wikimedia.org/wikipedia/commons/c/cd/Vector_Loading_fallback.gif'> Sending</div>",
    "</div>",
    "<div class='voting_bar'>",
      "<span class='oo-ui-widget oo-ui-widget-enabled oo-ui-inputWidget oo-ui-buttonElement oo-ui-buttonElement-framed oo-ui-labelElement oo-ui-flaggedElement-progressive oo-ui-flaggedElement-primary oo-ui-buttonInputWidget'>",
        "<input type='button' style='margin:auto !important;' tabindex='6' aria-disabled='false' id='confirm_review' value='Confirm reviews' class='oo-ui-inputWidget-input oo-ui-buttonElement-button'>",
      "</span>",
      "<div id='loading_'></div>",
      "<div id='confirm_'></div>",
      "<p style='padding-right:20%;float:right;margin:0'>",
        "<label for='filterNominations'>Hide descriptions</label>&nbsp;",
        "<input type='checkbox' id='toggleLi' onclick=\"$('div.qi').find('ul li:first').toggle();\">&nbsp;&nbsp;",
        "<label for='filterNominations'>Show Only</label>",
        "<select style='display:inline !important;width:auto;' id='filterNominations' class='submit ui-button ui-widget ui-state-default ui-corner-all ui-button-text-icon-primary ui-button-large'>",
          "<option value='Discuss'>Discuss</option>",
          "<option value='Promotion' style='color:DarkGreen;'>Promotion</option>",
          "<option value='Decline' style='color:DarkRed;'>Decline</option>",
          "<option value='Review'>Reviewed</option>",
          "<option value='ToReview'>To Review</option>",
          "<option value='' selected>All</option>",
        "</select>",
      "</p>",
    "</div>"
  ].join('');
  const COMBO_REVIEW = [
    "<select style='width:100%; margin:0px !important;' class='submit ui-button ui-widget ui-state-default ui-corner-all ui-button-text-icon-primary ui-button-large'>",
      "<option value='Promotion' style='color:DarkGreen;'>Promotion</option>",
      "<option value='Decline' style='color:DarkRed;'>Decline</option>",
      "<option value='Discuss' style='color:DarkGoldenRod;'>Discuss</option>",
      "<option value='Nomination'>Comment</option>",
    "</select>"
  ].join('');
  const NOTIFICATION_LAYOUT = [
    '<div class="mw-notification-content">',
      '<span class="ui-button-icon-primary ui-icon-green ui-icon ui-icon-check" style="display:inline-block"></span>&nbsp;',
      '<span style="font-size: 1em !important;color:green;">Your '
  ].join('');
let USERNAME = mw.config.get("wgUserName");
let TITLE = mw.config.get("wgPageName") === "Commons:Quality_images_candidates" ? "Commons:Quality_images_candidates/candidate_list" : mw.config.get("wgPageName");
const NOMINATION_CONTAINER = "div.qi";
const REVIEW_MESSAGE = "Dear " + USERNAME + ", please enter your review comment";
const api = new mw.Api();
const opt = {
	"Nomination": {color:"#0000FF", message:"", type:"Nomination"},
	"Promotion": {color:"lime", message:"Good quality.", type:"Promotion"},
	"Decline": {color:"red", message:"Please fill in a reason why the image does not meet the guidelines.", type:"Decline"},
	"Comment": {color:"#0000FF", message:"", type:"Comment"},
	"Discuss": {color:"#EEEE00", message:"I disagree.", type:"Discuss"}
};
let votes = {}, reviewCount = 0;
function _loading(show) {
	$("#loading_").text(show ? "Sending reviews..." : "");
	$("#confirm_").text("");
}
function _confirmMessage() {
	if (reviewCount > 0) $("#confirm_").text(reviewCount + " pending review confirmations");
}
function getButtonBg(val) {
	return val === "Decline" ? "red" : val === "Promotion" ? "green" : val === "Discuss" ? "yellow" : null;
}
function setComboClass(cmb) {
	let $cmb = $(cmb), btnClass = getButtonBg($cmb.val());
	$cmb.removeClass("ui-button-green ui-button-red ui-button-yellow");
	if (btnClass) $cmb.addClass("ui-button-" + btnClass);
}
async function getSectionContent(sectionNumber) {
	_loading(true);
	let data = await api.get({
		action: "query",
		prop: "revisions",
		rvprop: ["content", "timestamp"],
		titles: [TITLE],
		formatversion: "2",
		curtimestamp: true
	});
	_loading(false);
	return data.query.pages[0].revisions[0].content;
}
async function editSection(sectionNumber, content) {
	$(".loading_overly").toggle(500);
	try {
		let data = await $.ajax({
			url: mw.util.wikiScript("api"),
			data: {
				format: "json",
				action: "edit",
				title: TITLE,
				summary: "Reviewing " + reviewCount + " nomination(s) with [[Special:MyLanguage/Help:Gadget-QICvote|QICvote]]",
				text: content,
				token: mw.user.tokens.get("csrfToken")
			},
			dataType: "json",
			type: "POST"
		});
		if (data && data.edit && data.edit.result === "Success") {
			$(".loading_overly").toggle(500);
			_loading(false);
			votes = {};
			$("#confirm_").text("");
			mw.notify($(NOTIFICATION_LAYOUT + reviewCount + " review(s) have been saved.</span></div>"));
			reviewCount = 0;
		} else {
			mw.notify(data && data.error ? "Error: " + data.error.code + " : " + data.error.info : "Error: Unknown API result.");
		}
	} catch (e) {
		alert("Error: Request failed.");
	}
}
function getSectionId(element) {
	let url = $(element).closest("h2").find("span a:last").attr("href");
	let results = /[\?&]section=([^&#]*)/.exec(url);
	return (results && results[1]) || 0;
}
function _getImageName(aElement) {
	let parts = $(aElement).parent().find("a.image, a.mw-file-description").last().attr("href").split(":");
	return parts.length && parts[1] && parts[1].indexOf(".") !== -1 ? parts[1] : null;
}
function _remplaceLast(nomination, vote) {
	let newNom = getNominationType(nomination, vote.type);
	let newWord = getEOL(newNom) + getVoteTemplate(vote.type) + vote.comment + _getSignature() + "}}";
	let n = newNom.lastIndexOf("}}");
	return newNom.slice(0, n) + newNom.slice(n).replace("}}", newWord);
}
function escapeRegExp(str) {
	return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function getEOL(nom) {
	return nom.slice(-3) === "|}}" ? "" : "<br />";
}
function getVoteTemplate(type) {
	return type === "Promotion" ? "{{s}} " : type === "Decline" ? "{{o}} " : "";
}
function _getSignature() {
	let now = new Date(),
		month = ["January","February","March","April","May","June","July","August","September","October","November","December"],
		minute = now.getUTCMinutes() < 10 ? "0" + now.getUTCMinutes() : now.getUTCMinutes(),
		hour = now.getUTCHours() < 10 ? "0" + now.getUTCHours() : now.getUTCHours();
	return " --[[User:" + USERNAME + "|" + USERNAME + "]] " + hour + ":" + minute + ", " + now.getUTCDate() + " " + month[now.getUTCMonth()] + " " + now.getUTCFullYear() + " (UTC)";
}
function _addVote(content, vote) {
	let reg = escapeRegExp(vote.image) + "(.*)\\n",
		match = content.match(new RegExp(reg, "g"));
	if (!match) {
		reg = reg.replace(/_/g, ' ');
		match = content.match(new RegExp(reg, "g"));
	}
	if (!match) {
		console.log('Failed to find: ', vote.image);
		return;
	}
	let nomination = match[0];
	let newNom = _remplaceLast(nomination, vote);
	return content.replace(nomination, newNom);
}
function scapeFilenames(filesname) {
	return filesname.replace(/File:(.*?)\|\{\{+/g, m => m.replace(/\ /g, "_"));
}
function isDiscuss(oldType, newType) {
	return (oldType === "Promotion" && newType === "Decline") || (newType === "Promotion" && oldType === "Decline") || oldType === "Discuss";
}
function getNominationType(nom, newType) {
	function setType(oldType) {
		oldType = oldType.replace("{{/", "").replace("|", "");
		if (isDiscuss(oldType, newType)) newType = "Discuss";
		return "{{/" + newType + "|";
	}
	return nom.replace(/\{\{\/(Nomination|Promotion|Decline|Discuss)\|/g, setType);
}
async function addVotes() {
	for (let section in votes) {
		if (votes[section] && votes[section].length > 0) {
			let content = await getSectionContent(section - 1);
			content = scapeFilenames(content);
			for (let vote of votes[section]) content = _addVote(content, vote);
			await editSection(section, content);
		}
	}
}
function _getNominationContainer(el) {
	return $(el).parent().find(NOMINATION_CONTAINER);
}
function _nullVote(cmb) {
	if ($(cmb).val() === "") {
		_getNominationContainer(cmb).find("ul li i").text("Review needed");
		_getNominationContainer(cmb).css("border-color", "#0000FF");
		$(cmb).val("");
		return true;
	}
	return false;
}
function _getSelection(cmb) {
	let oldType = $(cmb).parent().find("div.qi ul li:nth-child(2)").children(":first").text(),
		newType = $(cmb).val();
	if (isDiscuss(oldType, newType)) newType = "Discuss";
	return opt[newType];
}
function _setBoderColorVote(cmb, color) {
	_getNominationContainer(cmb).css("border-color", color);
}
function _setVoteTitle(cmb, title) {
	_getNominationContainer(cmb).find("ul li:nth-child(2)").children(":first").text(title);
}
function _filterNominations(cmb) {
	let filter = $(cmb).val();
	$(".gallerybox").each(function() {
		let $box = $(this);
		$box.show();
		if (filter) {
			let $nom = $box.find(NOMINATION_CONTAINER).find("ul:first li:last"),
				reviewText = $nom.find("i").text().trim();
			if (reviewText === "Review needed") {
				if (filter !== "ToReview") $box.hide();
			} else if ($nom.find("b:first").text() !== filter) {
				$box.hide();
			}
		}
	});
}
function parseMediaWiki($cmb, wikiMarkup, callback) {
	api.get({
		action: 'parse',
		format: 'json',
		origin: '*',
		text: wikiMarkup + _getSignature()
	}).done(function(response) {
		let html = response.parse && response.parse.text ? response.parse.text["*"] : "";
		callback($cmb, html);
	}).fail(() => callback(null));
}
async function onChangeVote(cmb, section) {
	if (_nullVote(cmb)) return;
	let $cmb = $(cmb),
		selection = _getSelection(cmb),
		textOpts = selection.type === "Promotion" ? { value: selection.message } : { placeholder: selection.message },
		result = await OO.ui.prompt(REVIEW_MESSAGE, { textInput: textOpts, size: 'medium', escapable: true }).catch(() => null);
	if (!result || $.trim(result) === "") {
		$cmb.val("");
		return;
	}
	_setVoteTitle(cmb, selection.type);
	_setBoderColorVote(cmb, selection.color);
	setComboClass(cmb);
	parseMediaWiki($cmb, result, function($cmb, html) {
		$cmb.parent().find(NOMINATION_CONTAINER).find("ul").append(html);
	});
	let imageName = _getImageName(cmb);
	votes[section] = votes[section] || [];
	votes[section].push({ image: decodeURIComponent(imageName), comment: result, type: $cmb.val() });
	reviewCount++;
	_confirmMessage();
}
if (TITLE === "Commons:Quality_images_candidates/candidate_list") {
	$("#content").append(CONFIRM_BAR);
	$("#confirm_review").on("click", async function() { await addVotes(); });
	$(".gallerybox").each(function() {
		let $cmb = $(COMBO_REVIEW).val("");
		$(this).append($cmb);
		let section = getSectionId(this);
		$cmb.on("change", function() { onChangeVote(this, section); });
	});
	$("#filterNominations").change(function() {
		_filterNominations(this);
		setComboClass(this);
	});
}
});