« MediaWiki:Common.js » : différence entre les versions
Page de l’interface de MediaWiki
Autres actions
Aucun résumé des modifications |
Aucun résumé des modifications |
||
| (15 versions intermédiaires par le même utilisateur non affichées) | |||
| Ligne 149 : | Ligne 149 : | ||
}()); | }()); | ||
/* === Modèle:Règle — copie d'ancre === */ | |||
mw.hook('wikipage.content').add(function ($content) { | |||
/* Crée le toast une seule fois */ | |||
var $toast = $('#regle-toast'); | |||
if ($toast.length === 0) { | |||
$toast = $('<div>') | |||
.attr('id', 'regle-toast') | |||
.addClass('regle-toast') | |||
.appendTo('body'); | |||
} | |||
var toastTimer; | |||
function showToast(message) { | |||
clearTimeout(toastTimer); | |||
$toast.text(message).addClass('regle-toast--visible'); | |||
toastTimer = setTimeout(function () { | |||
$toast.removeClass('regle-toast--visible'); | |||
}, 2000); | |||
} | |||
/* Cible tous les liens internes dont le href commence par #r- */ | |||
$content.find('a[href^="#r-"]').off('click.regle').on('click.regle', function (e) { | |||
e.preventDefault(); | |||
var ancre = $(this).attr('href').replace('#', ''); | |||
var url = window.location.origin | |||
+ window.location.pathname | |||
+ '#' + ancre; | |||
/* Défilement vers l'ancre */ | |||
var cible = document.getElementById(ancre); | |||
if (cible) { | |||
cible.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |||
} | |||
/* Mise à jour de l'URL */ | |||
history.replaceState(null, '', '#' + ancre); | |||
/* Copie dans le presse-papier */ | |||
if (navigator.clipboard && window.isSecureContext) { | |||
navigator.clipboard.writeText(url) | |||
.then(function () { showToast('🔗 Lien copié !'); }) | |||
.catch(function () { showToast('❌ Copie impossible'); }); | |||
} else { | |||
/* Fallback navigateurs anciens */ | |||
var $tmp = $('<textarea>') | |||
.val(url) | |||
.css({ position: 'fixed', opacity: 0 }) | |||
.appendTo('body'); | |||
$tmp[0].select(); | |||
try { | |||
document.execCommand('copy'); | |||
showToast('Lien copié !'); | |||
} catch (err) { | |||
showToast('❌ Copie impossible'); | |||
} | |||
$tmp.remove(); | |||
} | |||
}); | |||
}); | |||
/* === Modèle:Règle — injection CSS du toast === */ | |||
(function () { | |||
if (document.getElementById('regle-toast-style')) return; | |||
var style = document.createElement('style'); | |||
style.id = 'regle-toast-style'; | |||
style.textContent = [ | |||
'.regle-toast {', | |||
' position: fixed;', | |||
' bottom: 2rem;', | |||
' left: 50%;', | |||
' transform: translateX(-50%) translateY(1rem);', | |||
' background: var(--color-surface-2);', | |||
' color: var(--color-base);', | |||
' border: 1px solid var(--color-surface-4);', | |||
' padding: 0.4rem 1rem;', | |||
' border-radius: var(--border-radius-pill, 2rem);', | |||
' font-size: 0.875rem;', | |||
' font-weight: 500;', | |||
' letter-spacing: 0.01em;', | |||
' opacity: 0;', | |||
' pointer-events: none;', | |||
' transition: opacity 0.25s ease, transform 0.25s ease;', | |||
' z-index: 200000;', | |||
' white-space: nowrap;', | |||
' box-shadow: var(--box-shadow-dialog, 0 4px 16px rgba(0,0,0,0.15));', | |||
' display: flex;', | |||
' align-items: center;', | |||
' gap: 0.4em;', | |||
'}', | |||
'.regle-toast.regle-toast--visible {', | |||
' opacity: 1;', | |||
' transform: translateX(-50%) translateY(0);', | |||
'}' | |||
].join('\n'); | |||
document.head.appendChild(style); | |||
}()); | |||
/* === fin injection CSS === */ | |||
/* === fin Modèle:Règle === */ | |||
/* ============================================================================= | |||
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 | |||
cards.forEach(card => observer.observe(card)); | |||
} else { | |||
// 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( '' ); | |||
} | |||
/** | |||
* 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' ); | |||
} | |||
} | |||
/** | |||
* 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' ); | |||
} | |||
} ); | |||
} ); | |||
} ); | |||
Dernière version du 11 mars 2026 à 19:24
/* Tout JavaScript présent ici sera exécuté par tous les utilisateurs à chaque chargement de page. */
$( function () {
$( '.citizen-search-trigger' ).on( 'click', function () {
$( '#searchInput' ).focus();
} );
// Raccourci Ctrl+K (ou Cmd+K sur Mac)
$( document ).on( 'keydown', function ( e ) {
if ( ( e.ctrlKey || e.metaKey ) && e.key === 'k' ) {
e.preventDefault();
$( '#searchInput' ).focus();
}
} );
} );
/**
* MinecraftConnect - Boutons de copie d'adresse serveur Minecraft
* Inspiré de l'extension PreToClip
*/
(function() {
'use strict';
function initMinecraftButtons($content) {
$content.find('.minecraft-connect-wrapper').each(function() {
var $wrapper = $(this);
// Éviter la double initialisation
if ($wrapper.data('mc-initialized')) {
return;
}
$wrapper.data('mc-initialized', true);
var serverAddress = $wrapper.data('mc-server');
var buttonText = $wrapper.data('mc-text');
// Créer le bouton
var $button = $('<button>')
.addClass('mw-ui-button mw-ui-progressive minecraft-connect-btn')
.attr('type', 'button')
.attr('title', 'Cliquer pour copier : ' + serverAddress)
.text(buttonText + ' 📋');
// Remplacer le span par le bouton
$wrapper.replaceWith($button);
// Gestion du clic
$button.on('click', function() {
copyToClipboard(serverAddress, $button, buttonText);
});
});
}
function copyToClipboard(text, $button, originalText) {
// Méthode moderne (Clipboard API)
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(
function() {
showSuccess($button, originalText);
},
function() {
// Fallback si échec
fallbackCopy(text, $button, originalText);
}
);
} else {
// Fallback pour anciens navigateurs
fallbackCopy(text, $button, originalText);
}
}
function fallbackCopy(text, $button, originalText) {
var $temp = $('<textarea>')
.val(text)
.css({
position: 'fixed',
top: 0,
left: 0,
width: '2em',
height: '2em',
padding: 0,
border: 'none',
outline: 'none',
boxShadow: 'none',
background: 'transparent'
})
.appendTo('body');
$temp[0].select();
$temp[0].setSelectionRange(0, 99999);
try {
var successful = document.execCommand('copy');
if (successful) {
showSuccess($button, originalText);
} else {
showError($button, originalText);
}
} catch (err) {
showError($button, originalText);
}
$temp.remove();
}
function showSuccess($button, originalText) {
mw.notify('Adresse copiée dans le presse-papier !', {
type: 'success',
autoHide: true,
tag: 'minecraft-connect'
});
$button
.text('✓ Copié !')
.removeClass('mw-ui-progressive')
.addClass('mw-ui-constructive')
.prop('disabled', true);
setTimeout(function() {
$button
.text(originalText + ' 📋')
.prop('disabled', false)
.removeClass('mw-ui-constructive')
.addClass('mw-ui-progressive');
}, 2000);
}
function showError($button, originalText) {
mw.notify('Erreur lors de la copie', {
type: 'error',
autoHide: true,
tag: 'minecraft-connect'
});
$button
.text('✗ Erreur')
.removeClass('mw-ui-progressive')
.addClass('mw-ui-destructive');
setTimeout(function() {
$button
.text(originalText + ' 📋')
.removeClass('mw-ui-destructive')
.addClass('mw-ui-progressive');
}, 2000);
}
// Initialisation au chargement et pour VisualEditor
mw.hook('wikipage.content').add(initMinecraftButtons);
}());
/* === Modèle:Règle — copie d'ancre === */
mw.hook('wikipage.content').add(function ($content) {
/* Crée le toast une seule fois */
var $toast = $('#regle-toast');
if ($toast.length === 0) {
$toast = $('<div>')
.attr('id', 'regle-toast')
.addClass('regle-toast')
.appendTo('body');
}
var toastTimer;
function showToast(message) {
clearTimeout(toastTimer);
$toast.text(message).addClass('regle-toast--visible');
toastTimer = setTimeout(function () {
$toast.removeClass('regle-toast--visible');
}, 2000);
}
/* Cible tous les liens internes dont le href commence par #r- */
$content.find('a[href^="#r-"]').off('click.regle').on('click.regle', function (e) {
e.preventDefault();
var ancre = $(this).attr('href').replace('#', '');
var url = window.location.origin
+ window.location.pathname
+ '#' + ancre;
/* Défilement vers l'ancre */
var cible = document.getElementById(ancre);
if (cible) {
cible.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
/* Mise à jour de l'URL */
history.replaceState(null, '', '#' + ancre);
/* Copie dans le presse-papier */
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(url)
.then(function () { showToast('🔗 Lien copié !'); })
.catch(function () { showToast('❌ Copie impossible'); });
} else {
/* Fallback navigateurs anciens */
var $tmp = $('<textarea>')
.val(url)
.css({ position: 'fixed', opacity: 0 })
.appendTo('body');
$tmp[0].select();
try {
document.execCommand('copy');
showToast('Lien copié !');
} catch (err) {
showToast('❌ Copie impossible');
}
$tmp.remove();
}
});
});
/* === Modèle:Règle — injection CSS du toast === */
(function () {
if (document.getElementById('regle-toast-style')) return;
var style = document.createElement('style');
style.id = 'regle-toast-style';
style.textContent = [
'.regle-toast {',
' position: fixed;',
' bottom: 2rem;',
' left: 50%;',
' transform: translateX(-50%) translateY(1rem);',
' background: var(--color-surface-2);',
' color: var(--color-base);',
' border: 1px solid var(--color-surface-4);',
' padding: 0.4rem 1rem;',
' border-radius: var(--border-radius-pill, 2rem);',
' font-size: 0.875rem;',
' font-weight: 500;',
' letter-spacing: 0.01em;',
' opacity: 0;',
' pointer-events: none;',
' transition: opacity 0.25s ease, transform 0.25s ease;',
' z-index: 200000;',
' white-space: nowrap;',
' box-shadow: var(--box-shadow-dialog, 0 4px 16px rgba(0,0,0,0.15));',
' display: flex;',
' align-items: center;',
' gap: 0.4em;',
'}',
'.regle-toast.regle-toast--visible {',
' opacity: 1;',
' transform: translateX(-50%) translateY(0);',
'}'
].join('\n');
document.head.appendChild(style);
}());
/* === fin injection CSS === */
/* === fin Modèle:Règle === */
/* =============================================================================
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
cards.forEach(card => observer.observe(card));
} else {
// 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( '' );
}
/**
* 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' );
}
}
/**
* 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' );
}
} );
} );
} );