« MediaWiki:Common.js » : différence entre les versions
Page de l’interface de MediaWiki
Autres actions
minicard |
Aucun résumé des modifications |
||
| (4 versions intermédiaires par le même utilisateur non affichées) | |||
| Ligne 255 : | Ligne 255 : | ||
/* === fin Modèle:Règle === */ | /* === fin Modèle:Règle === */ | ||
/* | /* ============================================================================= | ||
( function () { | EFFET D'APPARITION EN CASCADE POUR LES MINICARDS (Intersection Observer) | ||
' | ============================================================================= */ | ||
mw.hook('wikipage.content').add(function ($content) { | |||
// On cherche toutes les minicards de la page qui n'ont pas encore été animées | |||
const cards = $content[0].querySelectorAll('.minicard:not(.is-loaded)'); | |||
if (!cards.length) return; | |||
// Vérification de la compatibilité du navigateur | |||
if ('IntersectionObserver' in window) { | |||
let delay = 0; | |||
let resetDelayTimeout; | |||
const observer = new IntersectionObserver((entries, obs) => { | |||
entries.forEach(entry => { | |||
if (entry.isIntersecting) { | |||
// Si la carte entre dans l'écran, on applique un délai pour l'effet "cascade" | |||
setTimeout(() => { | |||
entry.target.classList.add('is-loaded'); | |||
}, delay); | |||
delay += 75; // 75ms de décalage entre chaque carte | |||
// On réinitialise le délai si on a fini de traiter un groupe de cartes | |||
clearTimeout(resetDelayTimeout); | |||
resetDelayTimeout = setTimeout(() => { delay = 0; }, 100); | |||
// On arrête d'observer cette carte pour ne l'animer qu'une seule fois | |||
obs.unobserve(entry.target); | |||
} | |||
}); | |||
}, { | |||
rootMargin: '0px 0px -30px 0px', // Déclenche l'animation 30px AVANT que la carte n'arrive en bas de l'écran | |||
threshold: 0.1 | |||
}); | |||
/* | // On observe chaque carte trouvée | ||
var | cards.forEach(card => observer.observe(card)); | ||
} else { | |||
if ( | // Fallback pour les très vieux navigateurs : on affiche tout direct | ||
cards.forEach(card => card.classList.add('is-loaded')); | |||
} | |||
}); | |||
/* ============================================================ | |||
* Nefald Page Search — Recherche dans le contenu de la page | |||
* ============================================================ */ | |||
/* --- Interception globale de Ctrl+F / Cmd+F --- */ | |||
$( document ).on( 'keydown.nefaldPageSearch', function ( e ) { | |||
if ( ( e.ctrlKey || e.metaKey ) && e.key.toLowerCase() === 'f' ) { | |||
var $searchInput = $( '.nefald-page-search__input' ); | |||
if ( $searchInput.length > 0 ) { | |||
e.preventDefault(); | |||
/* Scrolle vers la barre si elle n'est pas sticky ou visible */ | |||
$searchInput[0].scrollIntoView( { behavior: 'smooth', block: 'center' } ); | |||
/* Donne le focus et sélectionne le texte pour une frappe rapide */ | |||
$searchInput.focus().select(); | |||
} | |||
} | |||
} ); | |||
mw.hook( 'wikipage.content' ).add( function ( $content ) { | |||
$content.find( '.nefald-page-search-container' ).each( function () { | |||
var $container = $( this ); | |||
if ( $container.children().length > 0 ) { return; } | |||
/* Mise à jour du placeholder par défaut pour indiquer le raccourci */ | |||
var placeholder = $container.data( 'placeholder' ) || 'Rechercher dans la page (Ctrl+F)…'; | |||
/* --- Injection du HTML --- */ | |||
var html = | |||
'<div class="nefald-page-search">' + | |||
'<span class="nefald-page-search__icon">⌕</span>' + | |||
'<input class="nefald-page-search__input" type="search"' + | |||
' placeholder="' + placeholder + '"' + | |||
' aria-label="Rechercher dans la page"' + | |||
' autocomplete="off" spellcheck="false" />' + | |||
'<span class="nefald-page-search__counter"></span>' + | |||
'<button class="nefald-page-search__prev" title="Résultat précédent">↑</button>' + | |||
'<button class="nefald-page-search__next" title="Résultat suivant">↓</button>' + | |||
'<button class="nefald-page-search__clear" title="Effacer">✕</button>' + | |||
'</div>'; | |||
$container.append( html ); | |||
/* --- Références DOM --- */ | |||
var $input = $container.find( '.nefald-page-search__input' ); | |||
var $counter = $container.find( '.nefald-page-search__counter' ); | |||
var $prev = $container.find( '.nefald-page-search__prev' ); | |||
var $next = $container.find( '.nefald-page-search__next' ); | |||
var $clear = $container.find( '.nefald-page-search__clear' ); | |||
/* Zone dans laquelle on cherche : le contenu principal */ | |||
var $scope = $( '#mw-content-text .mw-parser-output' ); | |||
var matches = []; | |||
var currentIndex = -1; | |||
var timer = null; | |||
var HIGHLIGHT_CLASS = 'nefald-search-highlight'; | |||
var ACTIVE_CLASS = 'nefald-search-highlight--active'; | |||
/* --- Fonctions utilitaires --- */ | |||
/** | |||
* Parcourt récursivement les nœuds texte d'un élément | |||
* en ignorant les scripts, styles et le widget lui-même. | |||
*/ | |||
function getTextNodes( root ) { | |||
var nodes = []; | |||
var walker = document.createTreeWalker( | |||
root, | |||
NodeFilter.SHOW_TEXT, | |||
{ | |||
acceptNode: function ( node ) { | |||
var parent = node.parentNode; | |||
if ( !parent ) { return NodeFilter.FILTER_REJECT; } | |||
var tag = parent.nodeName.toLowerCase(); | |||
if ( tag === 'script' || tag === 'style' || tag === 'textarea' || tag === 'noscript' ) { | |||
return NodeFilter.FILTER_REJECT; | |||
} | |||
/* Ignorer le widget de recherche lui-même */ | |||
if ( $( parent ).closest( '.nefald-page-search-container' ).length ) { | |||
return NodeFilter.FILTER_REJECT; | |||
} | |||
/* Ignorer les nœuds vides */ | |||
if ( node.nodeValue.trim() === '' ) { | |||
return NodeFilter.FILTER_REJECT; | |||
} | |||
return NodeFilter.FILTER_ACCEPT; | |||
} | |||
} | |||
); | |||
while ( walker.nextNode() ) { | |||
nodes.push( walker.currentNode ); | |||
} | } | ||
return nodes; | |||
} | |||
/** | |||
* Supprime tous les surlignages existants en restaurant le texte d'origine. | |||
*/ | |||
function clearHighlights() { | |||
$scope.find( '.' + HIGHLIGHT_CLASS ).each( function () { | |||
var parent = this.parentNode; | |||
parent.replaceChild( document.createTextNode( this.textContent ), this ); | |||
parent.normalize(); | |||
} ); | |||
matches = []; | |||
currentIndex = -1; | |||
$counter.text( '' ); | |||
} | } | ||
/* | /** | ||
if ( | * Cherche `query` dans tous les nœuds texte et entoure | ||
* chaque occurrence d'un <mark>. | |||
*/ | |||
function doSearch( query ) { | |||
clearHighlights(); | |||
if ( !query || query.length < 2 ) { return; } | |||
/* Échapper les caractères spéciaux pour la RegExp */ | |||
var escaped = query.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ); | |||
var regex = new RegExp( '(' + escaped + ')', 'gi' ); | |||
var textNodes = getTextNodes( $scope[ 0 ] ); | |||
textNodes.forEach( function ( node ) { | |||
var text = node.nodeValue; | |||
if ( !regex.test( text ) ) { return; } | |||
regex.lastIndex = 0; /* reset après test */ | |||
var frag = document.createDocumentFragment(); | |||
var lastIndex = 0; | |||
var match; | |||
while ( ( match = regex.exec( text ) ) !== null ) { | |||
/* Texte avant le match */ | |||
if ( match.index > lastIndex ) { | |||
frag.appendChild( document.createTextNode( text.slice( lastIndex, match.index ) ) ); | |||
} | |||
/* Le match lui-même dans un <mark> */ | |||
var mark = document.createElement( 'mark' ); | |||
mark.className = HIGHLIGHT_CLASS; | |||
mark.textContent = match[ 1 ]; | |||
frag.appendChild( mark ); | |||
lastIndex = regex.lastIndex; | |||
} | |||
/* Texte restant après le dernier match */ | |||
if ( lastIndex < text.length ) { | |||
frag.appendChild( document.createTextNode( text.slice( lastIndex ) ) ); | |||
} | |||
node.parentNode.replaceChild( frag, node ); | |||
} ); | |||
matches = $scope.find( '.' + HIGHLIGHT_CLASS ).toArray(); | |||
if ( matches.length > 0 ) { | |||
currentIndex = 0; | |||
goTo( 0 ); | |||
} else { | |||
$counter.text( '0 résultat' ); | |||
} | |||
} | } | ||
return | /** | ||
* Navigue vers le match d'index donné. | |||
*/ | |||
function goTo( index ) { | |||
if ( matches.length === 0 ) { return; } | |||
/* Retirer la classe active du précédent */ | |||
$( matches ).removeClass( ACTIVE_CLASS ); | |||
/* Boucler */ | |||
if ( index < 0 ) { index = matches.length - 1; } | |||
if ( index >= matches.length ) { index = 0; } | |||
currentIndex = index; | |||
var el = matches[ currentIndex ]; | |||
$( el ).addClass( ACTIVE_CLASS ); | |||
/* Scroller vers l'élément actif */ | |||
el.scrollIntoView( { behavior: 'smooth', block: 'center' } ); | |||
$counter.text( ( currentIndex + 1 ) + ' / ' + matches.length ); | |||
} | } | ||
/* --- Événements --- */ | |||
$input.on( 'input', function () { | |||
clearTimeout( timer ); | |||
var query = $input.val(); | |||
/* Debounce de 250 ms pour ne pas spammer sur chaque frappe */ | |||
timer = setTimeout( function () { | |||
doSearch( query ); | |||
}, 250 ); | |||
} ); | } ); | ||
$next.on( 'click', function () { | |||
goTo( currentIndex + 1 ); | |||
} ); | |||
$prev.on( 'click', function () { | |||
goTo( currentIndex - 1 ); | |||
} ); | |||
$clear.on( 'click', function () { | |||
$input.val( '' ).trigger( 'focus' ); | |||
clearHighlights(); | |||
} ); | |||
/* Raccourcis clavier dans l'input */ | |||
$input.on( 'keydown', function ( e ) { | |||
if ( e.key === 'Enter' ) { | |||
e.preventDefault(); | |||
if ( e.shiftKey ) { | |||
goTo( currentIndex - 1 ); | |||
} else { | |||
goTo( currentIndex + 1 ); | |||
} | |||
} | |||
if ( e.key === 'Escape' ) { | |||
$input.val( '' ); | |||
clearHighlights(); | |||
$input.trigger( 'blur' ); | |||
} | |||
} ); | |||
} ); | } ); | ||
} ); | |||
} | |||