MediaWiki:Gadget-Descriptions.js
Erscheinungsbild
Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.
- Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
- Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
- Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
/* Gadget: Descriptions – Wikibase-Beschreibung beim Hover (Q/P/L) – nummernbasiert
* Abhängigkeiten: mediawiki.api, jquery.wikibase.wbtooltip
* Delegiert Events im Capture-Modus; erkennt Links via ID (Q/P/L) + Namensraum-Nummern.
*/
( function ( mw, $ ) {
'use strict';
var CFG = window.DescriptionsConfig || { nsItem: 120, nsProperty: 122, nsLexeme: 146 };
var DBG = !!window.DescriptionsDebug;
var cache = Object.create( null ); // id -> { text: string|false, pending: Promise|null }
var userLang = mw.config.get( 'wgUserLanguage' );
var nsIds = mw.config.get( 'wgNamespaceIds' ) || {}; // { 'qwasi':120, 'property':122, ... }
function log() { if ( DBG && window.console ) { console.log.apply( console, arguments ); } }
function extractId( href ) {
// …/Q123, …/P12, …/L7 – robust für /entity/, Special:EntityPage, und <NS>:Q123
var m = ( href || '' ).match( /(?:^|[/:])(Q\d+|P\d+|L\d+)(?:[^\d]|$)/ );
return m ? m[ 1 ] : null;
}
function titleFromHref( href ) {
// Versuche, den *Seitentitel* aus einem internen Link zu gewinnen, um NS zu bestimmen
try {
var a = document.createElement( 'a' );
a.href = href;
// Externe Links ignorieren
if ( a.host && a.host !== location.host ) { return null; }
// 1) ?title=... (script path)
var params = new URLSearchParams( a.search || '' );
if ( params.has( 'title' ) ) {
return decodeURIComponent( params.get( 'title' ) );
}
// 2) ArticlePath (/ $1 oder /wiki/$1)
var base = ( mw.config.get( 'wgArticlePath' ) || '/$1' ).replace( '$1', '' );
var path = a.pathname || '';
if ( base && path.indexOf( base ) === 0 ) {
return decodeURIComponent( path.slice( base.length ) );
}
// 3) Fallback: nichts
return null;
} catch ( e ) { return null; }
}
function nsIdFromTitle( titleText ) {
// Nimmt den Teil vor dem ersten ":" und schlägt in wgNamespaceIds nach (nummernbasiert)
if ( !titleText || titleText.indexOf( ':' ) < 0 ) { return 0; }
var nsName = titleText.split( ':', 1 )[ 0 ].trim().toLowerCase().replace( / /g, '_' );
return typeof nsIds[ nsName ] === 'number' ? nsIds[ nsName ] : null;
}
function isAllowedEntityLink( a ) {
if ( !a || a.nodeName !== 'A' ) { return false; }
var href = a.getAttribute( 'href' ) || '';
// Schnelle Negativfilter
if ( href.indexOf( '#' ) === 0 || /^mailto:|^javascript:/i.test( href ) ) {
return false;
}
var id = extractId( href );
if ( !id ) { return false; }
// Wenn wir den Titel erkennen, prüfen wir *nummernbasiert* den NS
var t = titleFromHref( href );
if ( t ) {
var ns = nsIdFromTitle( t );
if ( ns === CFG.nsItem || ns === CFG.nsProperty || ns === CFG.nsLexeme ) {
return { ok: true, id: id };
}
// Titel erkannt, aber NS nicht passend -> kein Entity-Link für uns
return false;
}
// Kein Titel ermittelbar (z. B. /entity/Q123, Special:EntityPage/Q123):
// Dann verlassen wir uns auf die ID – das ist in der Praxis korrekt für Wikibase-UI-Links.
return { ok: true, id: id };
}
function ensureDescription( id ) {
if ( cache[ id ] ) {
return cache[ id ].pending || Promise.resolve( cache[ id ].text || '' );
}
var api = new mw.Api( { userAgent: 'Gadget-Descriptions/local-2025-12-02' } );
cache[ id ] = { text: '', pending: null };
cache[ id ].pending = api.get( {
action: 'wbgetentities',
ids: id,
props: 'descriptions',
languages: userLang,
languagefallback: 1
} ).then( function ( data ) {
var ents = ( data && data.entities ) || {};
var e = ents[ id ];
var d = e && e.descriptions && ( e.descriptions[ userLang ] || Object.values( e.descriptions )[ 0 ] );
var text = d && d.value || '';
cache[ id ].text = text || false;
cache[ id ].pending = null;
log( '[Descriptions] loaded', id, '→', text );
return text;
}, function ( err ) {
cache[ id ].text = false;
cache[ id ].pending = null;
log( '[Descriptions] api error', id, err );
return '';
} );
return cache[ id ].pending;
}
function showTooltip( a, id, text ) {
var $a = $( a );
var $content = $( '<span>' );
$( '<strong>' ).text( id ).appendTo( $content );
if ( text ) {
$content.append( document.createTextNode( ' — ' + text ) );
}
$a.wbtooltip( {
content: $content,
permanent: true,
gravity: 'nw'
} );
var tip = $a.data( 'wbtooltip' );
if ( tip ) { tip.show(); }
}
function hideTooltip( a ) {
var tip = $( a ).data( 'wbtooltip' );
if ( tip ) { tip.hide(); }
}
// Event-Delegation in der *Capture*-Phase, damit andere Skripte uns nicht blockieren
function handleOver( ev ) {
var a = ev.target && ev.target.closest ? ev.target.closest( 'a' ) : null;
var res = isAllowedEntityLink( a );
if ( !res || !res.ok ) { return; }
var id = res.id;
// Cache direkt nutzen
if ( cache[ id ] && cache[ id ].text ) {
showTooltip( a, id, cache[id].text );
return;
}
// ggf. laufende Anfrage anhängen, sonst neu anfordern
( cache[ id ] && cache[ id ].pending ? cache[ id ].pending : ensureDescription( id ) )
.then( function ( text ) { showTooltip( a, id, text || '' ); } );
}
function handleOut( ev ) {
var a = ev.target && ev.target.closest ? ev.target.closest( 'a' ) : null;
if ( !a ) { return; }
hideTooltip( a );
}
// Registrieren (mouseover/mouseout & pointerover/pointerout als Fallbacks)
document.addEventListener( 'mouseover', handleOver, true );
document.addEventListener( 'mouseout', handleOut, true );
document.addEventListener( 'pointerover', handleOver, true );
document.addEventListener( 'pointerout', handleOut, true );
if ( DBG ) { log( '[Descriptions] ready. NS:', CFG ); }
}( mediaWiki, jQuery ) );