User:Dr vulpes/Scripts/UserHoverStats.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:Dr vulpes/Scripts/UserHoverStats. |
// <nowiki>
( function () {
/**
* Little popup script showing user stats on hover. I'm still learning Javascript so if things are silly or hacky ¯\_(ツ)_/¯
*/
class UserHoverStats {
constructor( $, mw, window ) {
this.$ = $;
this.mw = mw;
this.window = window;
this.userInfoCache = {};
this.$currentPopup = null;
}
async execute() {
const $links = this.$( '#mw-content-text a, #bodyContent a' );
$links.each( ( _, link ) => {
const $link = this.$( link );
if ( this.isUserPageLink( $link ) ) {
this.bindHoverEvent( $link );
}
} );
}
isUserPageLink( $link ) {
// Basic checks: must have href, must link to User: or User_talk:
const href = $link.attr( 'href' );
if ( !href ) return false;
const isUser = href.includes( '/wiki/User:' ) || href.includes( '?title=User:' );
const isUserTalk = href.includes( '/wiki/User_talk:' ) || href.includes( '?title=User_talk:' );
return ( isUser || isUserTalk );
}
bindHoverEvent( $link ) {
$link.on( 'mouseenter.UserHoverStats', async ( e ) => {
// Get the username by from the link
const userName = this.extractUserFromLink( $link );
if ( !userName ) {
return;
}
// Get user data (editcount, block status, etc.)
let userData;
try {
userData = await this.getUserStats( userName );
} catch ( err ) {
return;
}
// Show near hovered link
this.showPopup( $link, userData );
} );
// On mouseleave: hide any visible popup
$link.on( 'mouseleave.UserHoverStats', () => {
this.hidePopup();
} );
}
extractUserFromLink( $link ) {
try {
// Normalize from URL
const href = $link.attr( 'href' );
const urlObj = new this.mw.Uri( href, { strictMode: false } );
let title = this.mw.util.getParamValue( 'title', href );
if ( !title && urlObj.path.startsWith( '/wiki/' ) ) {
title = decodeURIComponent( urlObj.path.replace( /^\/wiki\//, '' ) );
}
if ( !title ) return null;
const titleObj = new this.mw.Title( title );
if ( ![2,3].includes( titleObj.getNamespaceId() ) ) {
return null;
}
return titleObj.getMainText(); // The username portion
} catch {
return null;
}
}
async getUserStats( userName ) {
// Return cached if available
if ( this.userInfoCache[ userName ] ) {
return this.userInfoCache[ userName ];
}
const userData = {
userName,
editCount: 0,
articlesCreated: 0,
blocked: false,
blockReason: ''
};
// 1) Fetch user info: editcount, blockinfo
let userResponse = await new this.mw.Api().get( {
action: 'query',
list: 'users',
ususers: userName,
usprop: 'blockinfo|editcount|registration',
format: 'json'
} );
const userObj = ( userResponse?.query?.users && userResponse.query.users[0] ) || null;
if ( userObj ) {
userData.editCount = userObj.editcount || 0;
if ( userObj.blockid ) {
userData.blocked = true;
userData.blockReason = userObj.blockreason || '';
}
}
// Approximate “articles created” by looking at new contributions in namespace 0
// One request, no continuation. Adjust this as needed.
// I am not done with this yet so if there are problems that's my bad
let contribResponse = await new this.mw.Api().get( {
action: 'query',
list: 'usercontribs',
ucuser: userName,
ucshow: 'new', // only show newly created pages
ucnamespace: 0, // article namespace only
uclimit: 50000, // change this limit if needed I don't have a better way to do this right now
format: 'json'
} );
const contribs = contribResponse?.query?.usercontribs || [];
userData.articlesCreated = contribs.length;
// Cache the result
this.userInfoCache[ userName ] = userData;
return userData;
}
showPopup( $link, userData ) {
// Remove any existing popup
this.hidePopup();
const $popup = this.$( '<div>' )
.addClass( 'userHoverPopup' )
.css( {
position: 'absolute',
zIndex: 9999,
background: '#fff',
border: '1px solid #444',
padding: '5px',
maxWidth: '250px',
fontSize: '90%'
} );
// Bring the message together
let contentHtml = `
<b>${this.$.escapeSelector( userData.userName )}</b><br>
Edits: ${userData.editCount}<br>
Articles created: ${userData.articlesCreated}<br>
`;
if ( userData.blocked ) {
contentHtml += `<span style="color:red;">Blocked</span><br>
Reason: ${this.$.escapeSelector( userData.blockReason )}<br>`;
} else {
contentHtml += 'Not currently blocked<br>';
}
$popup.html( contentHtml );
const offset = $link.offset();
$popup.css( {
top: offset.top + $link.height() + 5,
left: offset.left
} );
this.$( 'body' ).append( $popup );
this.$currentPopup = $popup;
}
hidePopup() {
if ( this.$currentPopup ) {
this.$currentPopup.remove();
this.$currentPopup = null;
}
}
}
mw.hook( 'wikipage.content' ).add( async () => {
await mw.loader.using( [
'mediawiki.util',
'mediawiki.Uri',
'mediawiki.Title',
'mediawiki.api'
], () => {
( new UserHoverStats( window.jQuery, mw, window ) ).execute();
} );
} );
mw.hook( 'postEdit' ).add( async () => {
await mw.loader.using( [
'mediawiki.util',
'mediawiki.Uri',
'mediawiki.Title',
'mediawiki.api'
], () => {
( new UserHoverStats( window.jQuery, mw, window ) ).execute();
} );
} );
}() );
// </nowiki>