User:Dragoniez/markblocked.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:Dragoniez/markblocked. |
/*
You can import this gadget to other wikis by using mw.loader.load and specifying the local alias for Special:Contributions. For example:
window.markblocked_contributions = 'Special:Contributions';
mw.loader.load('//en.wikipedia.org/w/index.php?title=MediaWiki:Gadget-markblocked.js&bcache=1&maxage=259200&action=raw&ctype=text/javascript');
Note that window.markblocked_contributions is a regex string; hence a value like 'Special:Contrib(?:ution)?s' will also work.
However, do not make any capturing group in the regex string; otherwise the gadget will throw an error.
This gadget will pull the user accounts and IPs from the history page and will strike out the users that are currently blocked.
Configuration variables:
- window.markblocked_contributions - Let wikis that are importing this gadget specify the local alias of Special:Contributions
- window.mbIndefStyle - custom CSS to override default CSS for indefinite blocks
- window.mbNoAutoStart - if set to true, doesn't mark blocked until you click "XX" in the "More" menu
- window.mbPartialStyle - custom CSS to override default CSS for partial blocks
- window.mbTempStyle - custom CSS to override default CSS for short duration blocks
- window.mbTipBox - if set to true, loads a yellow box with a pound sign next to blocked usernames. upon hovering over it, displays a tooltip.
- window.mbTipBoxStyle - custom CSS to override default CSS for the tip box (see above)
- window.mbTooltip - custom pattern to use for tooltips. default is '; blocked ($1) by $2: $3 ($4 ago)'
*/
// @ts-check
/* global mw */
//<nowiki>
( () => {
function execute() {
if ( [ 'edit', 'submit' ].indexOf( mw.config.get( 'wgAction' ) ) !== -1 ) {
return;
}
$.when( $.ready, mw.loader.using( [ 'mediawiki.util', 'mediawiki.Title', 'mediawiki.api' ] ) ).then( () => {
mw.util.addCSS( '\
.markblocked-loading a.userlink {opacity:' + ( window.mbLoadingOpacity || 0.85 ) + '}\
a.user-blocked-temp {' + ( window.mbTempStyle || 'opacity: 0.7; text-decoration: line-through' ) + '}\
a.user-blocked-indef {' + ( window.mbIndefStyle || 'opacity: 0.4; font-style: italic; text-decoration: line-through' ) + '}\
a.user-blocked-partial {' + ( window.mbPartialStyle || 'text-decoration: underline; text-decoration-style: dotted' ) + '}\
.user-blocked-tipbox {' + ( window.mbTipBoxStyle || 'font-size:smaller; background:#FFFFF0; border:1px solid #FEA; padding:0 0.3em; color:#AAA' ) + '}\
' );
if ( window.mbNoAutoStart ) {
const portletLink = mw.util.addPortletLink(
document.getElementById( 'p-cactions' ) ? 'p-cactions' : 'p-personal', // minerva doesn't have p-cactions
'',
'XX',
'ca-showblocks'
);
if ( !portletLink ) {
mw.notify('Failed to create a portlet link for markblocked.', { type: 'error', autoHideSeconds: 'long' } );
return;
}
portletLink.addEventListener( 'click', function( e ) {
e.preventDefault();
this.remove(); // Remove the link right away
hook();
});
} else {
hook();
}
} );
}
/**
* @typedef {object} LinkRegex
* @property {RegExp} article `/wiki/(TITLE)`
* @property {RegExp} script `/w/index.php?title=(TITLE)`
* @property {RegExp} user For all titles that are User:| User_talk: | Special:Contributions/ (`$2`: user name)
*/
function hook() {
// Get all aliases for user: & user_talk:
const userNS = [];
const wgNamespaceIds = mw.config.get( 'wgNamespaceIds' );
for ( const ns in wgNamespaceIds ) {
switch ( wgNamespaceIds[ ns ] ) {
// case -1:
// TODO: We should also collect aliases for special:
// break;
case 2:
case 3:
userNS.push( mw.util.escapeRegExp( ns.replace( /_/g, ' ' ) ) + ':' );
break;
default:
}
}
// Let wikis that are importing this gadget specify the local alias of Special:Contributions
const contribs = typeof window.markblocked_contributions === 'string' ? window.markblocked_contributions : 'Special:Contrib(?:ution)?s' ;
// RegExp for links
/**
* @type {LinkRegex}
*/
const rg = {
article: new RegExp( mw.config.get( 'wgArticlePath' ).replace( '$1', '' ) + '([^#]+)' ),
script: new RegExp( '^' + mw.config.get( 'wgScript' ) + '\\?title=([^#&]+)' ),
user: new RegExp( '^(' + userNS.join( '|' ) + '|' + contribs + '\\/)+([^\\/#]+)$', 'i' )
};
let firstTime = true;
mw.hook( 'wikipage.content' ).add( ( $container ) => {
// On the first call after initial page load, container is mw.util.$content
// Limit mainspace activity to just the diff definitions
if ( mw.config.get( 'wgAction' ) === 'view' && mw.config.get( 'wgNamespaceNumber' ) === 0 ) {
$container = $container.find( '.diff-title' );
}
if ( firstTime ) {
firstTime = false;
// On page load, also update the namespace tab
$container = $container.add( '#ca-nstab-user' );
}
markBlocked( $container, rg );
} );
}
/**
* @param {JQuery<HTMLElement>} $container
* @param {LinkRegex} rg
* @returns
*/
function markBlocked( $container, rg ) {
// Collect all user links in the page's content
/**
* @type {Record<string, HTMLAnchorElement[]>}
*/
const userLinks = Object.create(null);
$container.find( 'a' ).not( '.mw-changeslist-date, .ext-discussiontools-init-timestamplink, .mw-history-undo > a, .mw-rollback-link > a' ).each( function() {
const url = $( this ).attr( 'href' ); // This mustn't be replaced with this.href nor $( this ).prop, which return a URL with the protocol and host
if ( !url ) {
return;
}
let m, pageTitle;
if ( ( m = rg.article.exec( url ) ) ) {
pageTitle = m[ 1 ];
} else if ( ( m = rg.script.exec( url ) ) ) {
pageTitle = m[ 1 ];
} else {
return;
}
pageTitle = decodeURIComponent( pageTitle ).replace( /_/g, ' ' );
if ( !( m = rg.user.exec( pageTitle ) ) ) {
return;
}
const userTitle = mw.Title.newFromText( m[ 2 ] );
if ( !userTitle ) {
return;
}
let user = userTitle.getMainText();
if ( mw.util.isIPv6Address( user ) ) {
user = user.toUpperCase();
}
this.classList.add( 'userlink' );
if ( !userLinks[ user ] ) {
userLinks[ user ] = [];
}
userLinks[ user ].push( this );
} );
// Convert users into array
const users = Object.keys( userLinks );
if ( users.length === 0 ) {
return;
}
// API request
$container.addClass( 'markblocked-loading' );
const deferreds = [];
while ( users.length > 0 ) {
deferreds.push( markLinks( users.splice( 0, 50 ), userLinks ) );
}
$.when.apply( $, deferreds ).then( () => {
$container.removeClass( 'markblocked-loading' );
} );
}
/**
* @param {string[]} users
* @param {Record<string, HTMLAnchorElement[]>} userLinks
* @returns {JQueryProimse<void>}
*/
function markLinks ( users, userLinks ) {
return new mw.Api().post( {
action: 'query',
list: 'blocks',
bkusers: users.join( '|' ),
bkprop: 'user|by|timestamp|expiry|reason|restrictions', // no need for 'id|flags'
curtimestamp: true,
formatversion: '2'
} ).then( ( res ) => {
const list = res && res.query && res.query.blocks;
if ( !list ) {
return;
}
const serverTime = new Date( res.curtimestamp );
for ( let i = 0; i < list.length; i++ ) {
const block = list[ i ];
const partial = block.restrictions && !Array.isArray( block.restrictions ); // Partial block
let htmlClass, blockTime;
if ( /^in/.test( block.expiry ) ) {
htmlClass = partial ? 'user-blocked-partial' : 'user-blocked-indef';
blockTime = block.expiry;
} else {
htmlClass = partial ? 'user-blocked-partial' : 'user-blocked-temp';
// Apparently you can subtract date objects in JavaScript. Some kind of
// magic happens and they are automatically converted to milliseconds.
blockTime = inHours( parseTimestamp( block.expiry ) - parseTimestamp( block.timestamp ) );
}
let tooltipString = window.mbTooltip || '; blocked ($1) by $2: $3 ($4 ago)';
if ( partial ) {
tooltipString = tooltipString.replace( 'blocked', 'partially blocked' );
}
tooltipString = tooltipString.replace( '$1', blockTime )
.replace( '$2', block.by )
.replace( '$3', block.reason )
.replace( '$4', inHours( serverTime - parseTimestamp( block.timestamp ) ) );
const links = userLinks[ block.user ];
for ( let k = 0; links && k < links.length; k++ ) {
const $link = $( links[ k ] ).addClass( htmlClass );
if ( window.mbTipBox ) {
$( '<span class=user-blocked-tipbox>#</span>' ).attr( 'title', tooltipString ).insertBefore( $link );
} else {
$link.attr( 'title', $link.attr( 'title' ) + tooltipString );
}
}
}
} ).catch( ( _, err ) => {
console.error( err );
} );
}
/**
* @param {string} timestamp 20081226220605 or 2008-01-26T06:34:19Z
* @return {Date}
*/
function parseTimestamp( timestamp ) {
const matches = timestamp.replace( /\D/g, '' ).match( /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ );
return new Date( Date.UTC( matches[ 1 ], matches[ 2 ] - 1, matches[ 3 ], matches[ 4 ], matches[ 5 ], matches[ 6 ] ) );
}
/**
* @param {number} milliseconds 604800000
* @return {string} "2:30" or "5.06d" or "21d"
*/
function inHours( milliseconds ) {
let minutes = Math.floor( milliseconds / 60000 );
if ( !minutes ) {
return Math.floor( milliseconds / 1000 ) + 's';
}
let hours = Math.floor( minutes / 60 );
minutes = minutes % 60;
const days = Math.floor( hours / 24 );
hours = hours % 24;
if ( days ) {
return days + ( days < 10 ? '.' + addLeadingZeroIfNeeded( hours ) : '' ) + 'd';
}
return hours + ':' + addLeadingZeroIfNeeded( minutes );
}
/**
* @param {number} v 9
* @return {string} 09
*/
function addLeadingZeroIfNeeded( v ) {
if ( v <= 9 ) {
v = '0' + v;
}
return v;
}
execute();
} )();
//</nowiki>