User:A455bcd9/nominations viewer.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:A455bcd9/nominations viewer. |
// <nowiki>
// Nominations Viewer
//
// Description: Compact nominations for [[WP:FAC]], [[WP:FAR]], [[WP:FLC]],
// [[WP:FLRC]], [[WP:FPC]], and [[WP:PR]].
// Documentation: [[Wikipedia:Nominations Viewer]]
//
// ===
//
// Settings
// ---
//
// Default:
//
// NominationsViewer =
// {
// 'enabledPages': ['Wikipedia:Featured article candidates', ...],
// 'nominationData': ['images', 'age', 'nominators', 'participants', 'votes'],
// }
$(() => {
// Check the URL to determine if this script should be disabled.
if (window.location.href.includes('&disable=nomviewer')) {
return;
}
// Check if already ran elsewhere.
if (window.nominationsViewer) {
return;
}
window.nominationsViewer = true;
const NominationsViewer = window.NominationsViewer || {};
if (!NominationsViewer.enabledPages) {
NominationsViewer.enabledPages = {
'User:Gary/sandbox': 'nominations',
'Wikipedia:Featured article candidates': 'nominations',
'Wikipedia:Featured article review': 'reviews',
'Wikipedia:Featured list candidates': 'nominations',
'Wikipedia:Featured list removal candidates': 'reviews',
'Wikipedia:Featured picture candidates': 'pictures',
'Wikipedia:Peer review': 'peer reviews',
};
}
if (!NominationsViewer.nominationData) {
NominationsViewer.nominationData = [
'images',
'age',
'lastedit',
'nominators',
'participants',
'votes',
];
}
/**
* Add empty nomination data holders for a nomination.
*
* @param {string} pageName Name of the nomination page.
* @param {jQuery} $parentNode Parent node containing the entire nomination.
* @param {Array} ids The ID names to create.
* @returns {jQuery} The new node we added.
*/
function addNominationData(pageName, $parentNode, ids) {
return ids.map((id) => {
const $span = $(`<span id="${id}-${simplifyPageName(pageName)}"></span>`);
return $parentNode
.children()
.last()
.before($span);
});
}
function addAllNomInfo($headings) {
const data = { allH3Length: $headings.length };
const $expandAllLink = $(
'<a href="#" id="expand-all-link">expand all</a>'
).on('click', data, expandAllNoms);
const $collapseAllLink = $(
'<a href="#" id="collapse-all-link">collapse all</a>'
).on('click', data, collapseAllNoms);
const $info = $('<span class="overall-controls"></span>')
.append(' (')
.append($expandAllLink)
.append(' / ')
.append($collapseAllLink)
.append(')');
return $headings
.first()
.next()
.prevUntil('h2')
.last()
.prev()
.append($info);
}
/**
* Call the Wikipedia API with params then run a function on the return data.
*
* @param {Object} params The params to pass to the Wikipedia API.
* @param {Function} callback The function to run with the return data.
* @returns {undefined}
*/
function addNomData(params, callback) {
$.getJSON(mw.util.wikiScript('api'), {
format: 'json',
...params,
})
.done(callback)
.fail(() => {});
}
/**
* Add all data to a nomination.
*
* @param {string} pageName The page name.
* @returns {undefined}
*/
function addAllNomData(pageName) {
// Participants, age. Get all the edits for this nomination.
addNomData(
{
action: 'query',
prop: 'revisions',
rvdir: 'newer',
rvlimit: 500,
titles: pageName,
},
allRevisionsCallback
);
// Images, nominators, votes. Get the contents of the latest version of this
// nomination.
addNomData(
{
action: 'query',
prop: 'revisions',
rvdir: 'older',
rvlimit: 1,
rvprop: 'content',
titles: pageName,
},
currentRevisionCallback
);
}
/**
* Add data to a nomination.
*
* @param {Object} options Options
* @param {string} options.pageName The page name to which to add this data.
* @param {string} options.data The data to add.
* @param {string} options.id The ID of the field to add to.
* @param {string} options.hoverText Data that appears on hover.
* @returns {undefined}
*/
function addNewNomData({ pageName, data, id, hoverText }) {
if (!data) {
return;
}
// Select the element we want to add values to.
const $id = $(`#${id}-${simplifyPageName(pageName)}`);
const $newChild = $('<span class="nomv-data"></span>');
const $abbr = $(`<abbr title="${hoverText}">${data}</abbr>`);
$newChild.append($abbr);
$id.append($newChild);
}
/**
* Create the data that appears next to the nomination's listing.
*
* @param {string} pageName Page name of the nomination page.
* @returns {jQuery} The new node we added.
*/
function createData(pageName) {
const $newSpan = $('<span class="nomination-data"></span>').append(
'<span>(<span>'
);
const matchArchiveNumber = pageName.match(/(\d+)$/);
const conditions = matchArchiveNumber && matchArchiveNumber[1] > 1;
const matchArchiveNumberPrint = (() => {
if (conditions) {
const number = parseInt(matchArchiveNumber[1], 10);
const ordinalSuffix = (() => {
switch (number) {
case 2:
return 'nd';
case 3:
return 'rd';
default:
return 'th';
}
})();
return `: ${number}${ordinalSuffix}`;
}
return '';
})();
const $viewLink = $(
`<span><a href="${mw.util.getUrl(pageName)}">nomination</a>\
${matchArchiveNumberPrint}</span>`
);
return $newSpan.append($viewLink).append('<span>)<span>');
}
function createNewNode({ oldNode, showHideLink, newSpan, index }) {
const $newNode = $(`<div id="nom-title-${index}"></div>`).append(
oldNode.clone(true)
);
const $heading = $newNode.children().first();
$heading
.prepend(`<span class="nomination-order">${index + 1}.</span> `)
.append(' ')
.append(showHideLink)
.append(newSpan);
return $newNode;
}
/**
* Replace a nomination with a new and improved one.
*
* @param {Object} options Options
* @param {jQuery} options.$h3 The h3 heading of the nomination.
* @param {number} options.index The index of the nomination among the
* others.
* @returns {undefined}
*/
function createNomination({ $h3, index }) {
// Get edit links. It has to be an edit link, and not an article link,
// because it has to point to the nomination page, not the article.
const $editLinks = $h3.find('.mw-editsection a');
const useParentDiv = $editLinks.length === 0;
const parentDiv = $h3.parent();
const $editLinks2 = parentDiv.find('.mw-editsection a');
const $editLinksOption = useParentDiv ? $editLinks2 : $editLinks;
// There are no edit links.
if ($editLinksOption.length === 0) {
return;
}
const titleRegex = /[&?]title=(.*?)(?:&|$)/;
// Find the edit link that matches our regex.
const $filteredEditLinks = $editLinksOption.filter((elementIndex, element) =>
$(element)
.attr('href')
.match(titleRegex)
);
// Only continue if there are filtered edit links. They won't appear when a
// Peer Review is "too long" and therefore is replaced with a message to go
// to the review page directly. So, skip this nomination.
if (
$filteredEditLinks.length === 0 ||
!$filteredEditLinks.eq(0).attr('href') ||
!$filteredEditLinks
.eq(0)
.attr('href')
.match(titleRegex)
) {
return;
}
// Get the name of the nomination page.
const pageName = decodeURIComponent(
$filteredEditLinks
.eq(0)
.attr('href')
.match(titleRegex)[1]
);
// Create the [show] / [hide] link.
const showHideLink = createShowHideLink(index);
// Create the spot to put the data that we will retrieve via the Wikipedia
// API.
const newSpan = createData(pageName);
// Move the nomination into a hidden node.
hideNomination($h3, index);
// Add placeholders for the data that we will retrieve for the API.
addNominationData(pageName, newSpan, NominationsViewer.nominationData);
const nodeToReplace = useParentDiv ? parentDiv : $h3;
// Create the nomination's title line.
const newNode = createNewNode({
oldNode: nodeToReplace,
showHideLink,
newSpan,
index,
});
// Create the actual nomination
const nomDiv = generateNomination(index, newNode, nodeToReplace);
// Replace this nomination with the new one we created.
nodeToReplace.replaceWith(nomDiv);
// Ask the API to add data to our placeholders.
addAllNomData(pageName);
}
function createShowHideLink(index) {
const span = $('<span class="nomv-show-hide"></span>');
const link = $(`<a href="#" id="nom-button-${index}">show</a>`).on(
'click',
{ index },
toggleNomClick
);
return span
.append('[')
.append(link)
.append(']');
}
function generateNomination(index, newNode, oldNode) {
return $(`<div class="nomination" id="nom-${index}"></div>`)
.append(newNode.clone(true))
.append($(oldNode[0].nextSibling).clone(true));
}
// This function MUST stay in JavaScript, rather than switch to jQuery, for
// optmization reasons.
//
// The jQuery version slowed the page down by about 28%. This version slows
// the page down by about 11%, so it is about 17% faster.
function hideNomination($h3, index) {
// Re-create all nodes between this H3 node, and the next one, then place it
// into a new node.
const hiddenNode = document.createElement('div');
hiddenNode.className = 'nomination-body';
hiddenNode.id = `nom-data-${index}`;
hiddenNode.style.display = 'none';
let parentNode = $h3[0].parentNode;
let sectionStart = parentNode.classList.contains('mw-heading3') ? parentNode : $h3[0];
let nomNextSibling = sectionStart.nextSibling;
// Continue to the next node, as long as the next node still exists, it
// isn't an H2 or H3, and it doesn't have the class "printfooter or mw-heading2"
while (
nomNextSibling &&
!(
['H2', 'H3'].includes(nomNextSibling.nodeName) ||
(
nomNextSibling.childNodes &&
nomNextSibling.childNodes.length > 1 &&
['H2', 'H3'].includes(nomNextSibling.childNodes[1].nodeName)
)
) &&
!(
nomNextSibling.classList &&
nomNextSibling.classList.contains('printfooter')
) &&
!(
nomNextSibling.classList &&
nomNextSibling.classList.contains('mw-heading2')
) &&
!(
nomNextSibling.classList &&
nomNextSibling.classList.contains('mw-heading3')
)
) {
const nomNextSiblingTemporary = nomNextSibling.nextSibling;
// Move the node, if it isn't a text node
if (nomNextSibling.nodeType !== 3) {
// eslint-disable-next-line unicorn/prefer-node-append
hiddenNode.appendChild(nomNextSibling);
}
nomNextSibling = nomNextSiblingTemporary;
}
// Insert hidden content
return sectionStart.after(hiddenNode);
}
/**
* The main function, to run the script.
*
* @returns {undefined}
*/
function init() {
let currentPageIsASubpage;
let currentPageIsEnabled;
const pageName = mw.config.get('wgPageName');
// Check if enabled on this page
Object.keys(NominationsViewer.enabledPages).forEach((page) => {
if (pageName === page.replace(/\s/g, '_')) {
currentPageIsEnabled = true;
} else if (pageName.startsWith(page.replace(/\s/g, '_'))) {
currentPageIsASubpage = true;
}
});
if (
!currentPageIsEnabled ||
mw.config.get('wgAction') !== 'view' ||
window.location.href.includes('&oldid=') ||
currentPageIsASubpage
) {
return;
}
// Append the CSS now, since we're definitely running the script on this
// page.
addCss();
const $parentNode = $('.mw-content-ltr');
const $h3s = $parentNode.find('h3');
addAllNomInfo($h3s);
// Loop through each nomination
$h3s.each((index, element) =>
createNomination({
$h3: $(element),
index,
})
);
// Fix any conflicts with collapsed comments (using the special template).
$('.collapseButton').each((index, element) => {
const $link = $(element)
.children()
.first();
// eslint-disable-next-line unicorn/prefer-string-slice
const newIndex = $link
.attr('id')
.substring(
$link.attr('id').indexOf('collapseButton') + 'collapseButton'.length,
$link.attr('id').length
);
$link.attr('href', '#').on('click', { newIndex }, collapseTable);
});
}
// Helpers
function collapseTable(event) {
event.preventDefault();
const tableIndex = event.data.index;
const collapseCaption = 'hide';
const expandCaption = 'show';
const $button = $(`#collapseButton${tableIndex}`);
const $table = $(`#collapsibleTable${tableIndex}`);
if ($table.length === 0 || $button.length === 0) {
return false;
}
const $rows = $table.find('> tbody > tr');
if ($button.text() === collapseCaption) {
$rows.each((index, element) => {
if (index === 0) {
return true;
}
return $(element).hide();
});
return $button.text(expandCaption);
}
$rows.each((index, element) => {
if (index === 0) {
return true;
}
return $(element).show();
});
return $button.text(collapseCaption);
}
// Add CSS to the page, to use for this script. This is a separate function,
// so that it's more easy to disable it when necessary.
function addCss() {
mw.util.addCSS(`
#content .nomination h3 {
margin-bottom: 0;
padding-top: 0;
}
.nomination-data,
.nomination-order,
.overall-controls {
font-size: 75%;
font-weight: normal;
}
.nomination-order {
display: inline-block;
width: 25px;
}
.nomv-show-hide {
display: inline-block;
font-size: 13px;
font-weight: normal;
margin-right: 2.5px;
width: 40px;
}
.nomv-show-hide a {
display: inline-block;
text-align: center;
width: 31px;
}
.nomv-data::before {
content: " · ";
}
.nomv-data abbr {
white-space: nowrap;
}
`);
}
function expandAllNoms(event) {
return toggleAllNoms(event, 'expand');
}
function collapseAllNoms(event) {
return toggleAllNoms(event, 'collapse');
}
function toggleAllNoms(event, actionParam) {
let action = actionParam;
if (!action) {
action = 'expand';
}
event.preventDefault();
const { allH3Length } = event.data;
new Array(allH3Length).fill().forEach((value, index) => {
toggleNom(index, action);
});
}
function toggleNom(id, actionParam) {
let action = actionParam;
if (!action) {
action = '';
}
const toggleHideNom = ($node, $nomButton) => {
$node.hide();
return $nomButton.text('show');
};
const toggleShowNom = ($node, $nomButton) => {
$node.show();
return $nomButton.text('hide');
};
const $node = $(`#nom-data-${id}`);
const $nomButton = $(`#nom-button-${id}`);
// These are actions that override the status for all nominations.
if (action === 'collapse') {
return toggleHideNom($node, $nomButton);
}
if (action === 'expand') {
return toggleShowNom($node, $nomButton);
}
// These have to be separate from the above because they have a lower
// priority.
if ($node.is(':visible')) {
return toggleHideNom($node, $nomButton);
}
if ($node.is(':hidden')) {
return toggleShowNom($node, $nomButton);
}
return null;
}
function toggleNomClick(event) {
event.preventDefault();
const { index } = event.data;
return toggleNom(index);
}
// Callbacks
function addParticipants(revisions, pageName, queryContinue) {
if (!dataIsEnabled('participants') || !revisions) {
return;
}
const users = {};
let userCount = 0;
revisions.forEach((revision) => {
if (!revision.user) {
return;
}
if (users[revision.user]) {
users[revision.user] += 1;
} else {
users[revision.user] = 1;
userCount += 1;
}
});
const moreThan = queryContinue ? 'more than ' : '';
const usersArray = Object.keys(users).map((user) => [
user,
parseInt(users[user], 10),
]);
const usersArray2 = [...usersArray]
.sort((a, b) => {
if (a[1] < b[1]) {
return 1;
}
if (a[1] > b[1]) {
return -1;
}
return 0;
})
.map((user) => `${user[0]}: ${user[1]}`);
addNewNomData({
pageName,
data: `${moreThan + userCount} ${pluralize('participant', userCount)}`,
id: 'participants',
hoverText: `Sorted from most to least edits Total edits: ${
revisions.length
} Format: "editor: \
number of edits": ${usersArray2.join(' ')}`,
});
}
function allRevisionsCallback(object) {
const vars = formatJSON(object);
if (!vars) {
return;
}
// Participants
addParticipants(vars.revisions, vars.pageName, object['query-continue']);
// Nomination age
addAge(vars.firstRevision, vars.pageName);
// Last edit
addLastEdit(vars.lastRevision, vars.pageName);
}
function addImagesCount(content, pageName) {
if (!nomType('pictures') || !dataIsEnabled('images')) {
return;
}
// Determine number of images in the nomination
const pattern1 = /\[\[(file|image):.*?]]/gi;
const pattern2 = /\n(file|image):.*\|/gi;
const matches1 = content.match(pattern1);
const matches2 = content.match(pattern2);
const matches = matches1 || matches2 || [];
const images = matches.map((match) => {
const split = match.split('|');
const filename = $.trim(split[0].replace(/^\[\[/, ''));
return filename;
});
addNewNomData({
pageName,
data: `${matches.length} ${pluralize('image', matches.length)}`,
id: 'images',
hoverText: `Images (in order of appearance): ${images.join(
' '
)}`,
});
}
function getNominators(content) {
let nomTypeText = '';
let listOfNominators = {};
switch (nomType()) {
case 'nominations':
nomTypeText = 'nominator';
listOfNominators = findNominators(content, /Nominator(\(s\))?:.*/);
// No nominators were found, so try once more with a different pattern.
if ($.isEmptyObject(listOfNominators)) {
listOfNominators = findNominators(content, /:<small>''.*/);
}
break;
case 'reviews':
nomTypeText = 'notification';
listOfNominators = findNominators(content, /(Notified|Notifying):?.*/);
break;
case 'pictures':
nomTypeText = 'nominator';
listOfNominators = findNominators(
content,
/\* '''Support as nominator''' – .*/
);
break;
default:
}
return { listOfNominators, nomTypeText };
}
function addNominators(content, pageName) {
if (!dataIsEnabled('nominators') || nomType('peer reviews')) {
return;
}
const { listOfNominators, nomTypeText } = getNominators(content);
let allNominators = Object.keys(listOfNominators)
.map((n) => n)
.sort();
let data;
if (allNominators.length > 0) {
data = `${allNominators.length} ${pluralize(
nomTypeText,
allNominators.length
)}`;
// We couldn't identify any nominators.
} else {
// Use the first username on the page to determine the nominator.
const matches = content.match(/\[\[User:(.*?)[\]|]/);
if (nomType('nominations') && matches) {
allNominators = [matches[1]];
data = `${allNominators.length} ${pluralize(
nomTypeText,
allNominators.length
)}`;
// This is not a nomination-type, and we couldn't find any relevant
// users, so we have to assume that there are none.
} else {
data = `0 ${pluralize(nomTypeText, 0)}`;
}
}
addNewNomData({
pageName,
data,
id: 'nominators',
hoverText: `${pluralize(
capitalize(nomTypeText),
allNominators.length
)} (sorted alphabetically): ${allNominators.join(' ')}`,
});
}
/**
* Generate the patterns used to find vote text.
*
* @returns {Object} The patterns.
*/
function getVoteTextAndPatterns() {
// Look for text that is enclosed within bold text, or level-4 (or greater)
// headings.
const wrapPattern = "('''|====)";
// The amount of characters allowed between the vote text, and the wrapping
// patterns.
const voteBuffer = 25;
const textPattern = `(.{0,${voteBuffer}})?`;
let openPattern = `${wrapPattern}${textPattern}`;
let closePattern = `${textPattern}${wrapPattern}`;
let supportText = 'support';
let opposeText = 'oppose';
// Use different words for review pages.
if (nomType('reviews')) {
supportText = 'keep';
opposeText = 'delist';
// Pictures has their own specific method of declaring votes.
} else if (nomType('pictures')) {
openPattern = "\\*(\\s)?'''.*?";
closePattern = ".*?'''";
}
const createPattern = (text) =>
new RegExp(
`(${openPattern}${text}${closePattern}|^;${textPattern}${text})`,
'gim'
);
return {
supportText,
supportPattern: createPattern(supportText),
opposeText,
opposePattern: createPattern(opposeText),
};
}
function shouldShowVotes() {
const showOpposesForNominations = false;
const showOpposesForReviews = true;
return (
((nomType('nominations') || nomType('pictures')) &&
showOpposesForNominations) ||
(nomType('reviews') && showOpposesForReviews)
);
}
/**
* Add votes data to a nomination.
*
* @param {string} content The nomination's content.
* @param {string} pageName The page name.
* @returns {undefined}
*/
function addVotes(content, pageName) {
if (!dataIsEnabled('votes') || nomType('peer reviews')) {
return;
}
const {
supportText,
supportPattern,
opposeText,
opposePattern,
} = getVoteTextAndPatterns();
const supportMatches = content.match(supportPattern) || [];
const opposeMatches = content.match(opposePattern) || [];
const supports = `${supportMatches.length} ${pluralize(
supportText,
supportMatches.length
)}`;
const opposes = `, ${opposeMatches.length} ${pluralize(
opposeText,
opposeMatches.length
)}`;
addNewNomData({
pageName,
data: shouldShowVotes() ? supports + opposes : supports,
id: 'votes',
hoverText: supports + opposes,
});
}
function currentRevisionCallback(object) {
const vars = formatJSON(object);
if (!vars) {
return;
}
const content = vars.firstRevision ? vars.firstRevision['*'] : null;
if (!content) {
return;
}
// 'images'
addImagesCount(content, vars.pageName);
// 'nominators'
addNominators(content, vars.pageName);
// 'votes'
addVotes(content, vars.pageName);
}
function addAge(firstRevision, pageName) {
if (!dataIsEnabled('age') || !firstRevision) {
return;
}
const { timeAgo, then } = getTimeAgo(firstRevision.timestamp);
addNewNomData({
pageName,
data: timeAgo,
id: 'age',
hoverText: `Creation date (local time): ${then}`,
});
}
function addLastEdit(lastRevision, pageName) {
if (!dataIsEnabled('lastedit') || !lastRevision) {
return;
}
const { timeAgo, then } = getActivity(lastRevision.timestamp);
addNewNomData({
pageName,
data: timeAgo,
id: 'lastedit',
hoverText: `Last edit date (local time): ${then}`,
});
}
// Callback helpers
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
/**
* Check if the data field is enabled.
*
* @param {string} dataName The name of the data field to look up.
* @returns {boolean} The data field is enabled, so we want to use it.
*/
function dataIsEnabled(dataName) {
return NominationsViewer.nominationData.some((data) => dataName === data);
}
// Given `content`, find nominators with the `pattern`. Returns an Object, so
// that we exclude duplicates.
function findNominators(content, pattern) {
const nominatorMatches = content.match(pattern);
const listOfNominators = {};
if (!nominatorMatches) {
return listOfNominators;
}
// Find nominator usernames.
// [[User:Example|Example]], [[Wikipedia talk:WikiProject Example]]
let nominators = nominatorMatches[0].match(
/\[\[(user|wikipedia|wp|wt)([ _]talk)?:.*?]]/gi
);
if (nominators) {
nominators.forEach((nominator) => {
// Strip unneeded characters from the nominator's URL.
let username = nominator
// Strip the start of the username link.
.replace(/\[\[(user|wikipedia|wp|wt)([ _]talk)?:/i, '')
// Strip the displayed portion of the username link.
.replace(/\|.*/, '')
// Strip the ending portion of the username link.
.replace(']]', '')
// Strip URL anchors.
.replace(/#.*?$/, '');
// Does 'username' have a '/' that we have to strip?
if (username.includes('/')) {
username = username.slice(0, Math.max(0, username.indexOf('/')));
}
listOfNominators[username] += 1;
});
}
// {{user|Example}} and similar variants
const userTemplatePattern = /{{user.*?\|(.*?)}}/gi;
nominators = nominatorMatches[0].match(userTemplatePattern);
if (nominators) {
nominators.forEach((singleNominator) => {
listOfNominators[
singleNominator.replace(userTemplatePattern, '$1')
] += 1;
});
}
return listOfNominators;
}
function formatJSON(object) {
if (!object.query || !object.query.pages) {
return false;
}
const vars = [];
vars.pages = object.query.pages;
vars.page = Object.keys(vars.pages).map((page) => page);
if (vars.page.length !== 1) {
return false;
}
vars.page = object.query.pages[vars.page[0]];
vars.pageName = vars.page.title.replace(/\s/g, '_');
if (!vars.page.revisions) {
return false;
}
[vars.firstRevision] = vars.page.revisions;
[vars.lastRevision] = vars.page.revisions.slice(-1);
vars.revisions = vars.page.revisions;
return vars;
}
/**
* Check if the nomination type of the current nomination is the type
* specified. If no type is specified, then return the type of the current
* nomination. Possible types are: `nominations`, `peer reviews`, `pictures`,
* and `reviews`, as specified in `NominationsViewer.enabledPages`.
*
* @param {string} [type] The type to compare the current nomination with.
* @returns {boolean|string} The current nomination matches the type
* specified, or the type of the current nomination.
*/
function nomType(type = null) {
const pageName = mw.config.get('wgPageName').replace(/_/g, ' ');
const pageType = NominationsViewer.enabledPages[pageName];
if (type) {
return type === pageType;
}
return pageType;
}
/**
* Pluralize a word if necessary.
*
* @param {string} string The word to possibly pluralize.
* @param {number} count The number of items there are.
* @returns {string} The pluralized word.
*/
function pluralize(string, count) {
const plural = `${string}s`;
if (count === 1) {
return string;
}
return plural;
}
/**
* Format a page name by remove any non-word characters.
*
* @param {string} pageName The page name to format.
* @returns {string} The formatted page name.
*/
function simplifyPageName(pageName) {
return pageName.replace(/\W/g, '');
}
/**
* Given a timestamp, generally calculate the time ago.
*
* @param {string} timestamp A timestamp.
* @returns {Object.<string, string>} The time ago phrase.
*/
function getTimeAgo(timestamp) {
const matches = timestamp.match(
/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z/
);
const now = new Date();
const then = new Date(
Date.UTC(
matches[1],
matches[2] - 1,
matches[3],
matches[4],
matches[5],
matches[6]
)
);
const millisecondsAgo = now.getTime() - then.getTime();
const daysAgo = Math.floor(millisecondsAgo / (1000 * 60 * 60 * 24));
let timeAgo = '';
if (daysAgo > 0) {
const weeksAgo = Math.round(daysAgo / 7);
const monthsAgo = Math.round(daysAgo / 30);
const yearsAgo = Math.round(daysAgo / 365);
if (yearsAgo >= 1) {
timeAgo = `${yearsAgo} ${pluralize('year', yearsAgo)} old`;
} else if (monthsAgo >= 3) {
timeAgo = `${monthsAgo} ${pluralize('month', monthsAgo)} old`;
} else if (weeksAgo >= 1) {
timeAgo = `${weeksAgo} ${pluralize('week', weeksAgo)} old`;
} else {
timeAgo = `${daysAgo} ${pluralize('day', daysAgo)} old`;
}
} else {
timeAgo = 'today';
}
return { timeAgo, then };
}
function getActivity(timestamp) {
const matches = timestamp.match(
/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z/
);
const now = new Date();
const then = new Date(
Date.UTC(
matches[1],
matches[2] - 1,
matches[3],
matches[4],
matches[5],
matches[6]
)
);
const millisecondsAgo = now.getTime() - then.getTime();
const daysAgo = Math.floor(millisecondsAgo / (1000 * 60 * 60 * 24));
let timeAgo = '';
if (daysAgo > 0) {
const weeksAgo = Math.round(daysAgo / 7);
const monthsAgo = Math.round(daysAgo / 30);
const yearsAgo = Math.round(daysAgo / 365);
if (yearsAgo >= 1) {
timeAgo = `<b>Inactive for ${yearsAgo} ${pluralize('year', yearsAgo)}</b>`;
} else if (monthsAgo >= 3) {
timeAgo = `<b>Inactive for ${monthsAgo} ${pluralize('month', monthsAgo)}</b>`;
} else if (weeksAgo >= 1) {
timeAgo = `<b>Inactive for ${weeksAgo} ${pluralize('week', weeksAgo)}</b>`;
} else {
timeAgo = `Active ${daysAgo} ${pluralize('day', daysAgo)} ago`;
}
} else {
timeAgo = 'Active today';
}
return { timeAgo, then };
}
init();
});
// </nowiki>