Basculer le menu
Changer de menu des préférences
Basculer le menu personnel
Non connecté(e)
Votre adresse IP sera visible au public si vous faites des modifications.

« MediaWiki:Common.js » : différence entre les versions

Page de l’interface de MediaWiki
Hiob (discussion | contributions)
fallback
Hiob (discussion | contributions)
Aucun résumé des modifications
 
(3 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 === */


/* MiniCard Grid - Ajustement dynamique des colonnes */
/* =============================================================================
( function () {
  EFFET D'APPARITION EN CASCADE POUR LES MINICARDS (Intersection Observer)
     'use strict';
  ============================================================================= */
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;


     var MIN_CARD_WIDTH = 140;
     // Vérification de la compatibilité du navigateur
    var GAP = 12;
    if ('IntersectionObserver' in window) {
        let delay = 0;
        let resetDelayTimeout;


    function calcBestCols( total, containerWidth ) {
        const observer = new IntersectionObserver((entries, obs) => {
        var maxColsByWidth = Math.floor(
            entries.forEach(entry => {
            ( containerWidth + GAP ) / ( MIN_CARD_WIDTH + GAP )
                if (entry.isIntersecting) {
        );
                    // Si la carte entre dans l'écran, on applique un délai pour l'effet "cascade"
        maxColsByWidth = Math.max( 1, Math.min( maxColsByWidth, total ) );
                    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
        });


         var bestCols = 1;
        // On observe chaque carte trouvée
         for ( var n = 1; n <= maxColsByWidth; n++ ) {
        cards.forEach(card => observer.observe(card));
             if ( total % n === 0 ) {
    } else {
                 bestCols = n;
        // 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">&#x2315;</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">&#x2191;</button>' +
                '<button class="nefald-page-search__next" title="Résultat suivant">&#x2193;</button>' +
                '<button class="nefald-page-search__clear" title="Effacer">&#x2715;</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;
         }
         }


         if ( bestCols === 1 && maxColsByWidth > 1 ) {
         /**
             bestCols = maxColsByWidth;
        * 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( '' );
         }
         }


         return bestCols;
         /**
    }
        * 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 );


    function adjustGrid( grid ) {
            /* Scroller vers l'élément actif */
        var cards = grid.querySelectorAll( '.minicard' );
            el.scrollIntoView( { behavior: 'smooth', block: 'center' } );
        var total = cards.length;
        if ( total === 0 ) return;


        var containerWidth = grid.offsetWidth;
            $counter.text( ( currentIndex + 1 ) + ' / ' + matches.length );
        if ( containerWidth === 0 ) {
            requestAnimationFrame( function () { adjustGrid( grid ); } );
            return;
         }
         }


         var bestCols = calcBestCols( total, containerWidth );
         /* --- Événements --- */
        grid.style.gridTemplateColumns = 'repeat(' + bestCols + ', 1fr)';


         var cardWidth = ( containerWidth - ( bestCols - 1 ) * GAP ) / bestCols;
         $input.on( 'input', function () {
        var rowHeight = cardWidth < 120 ? '48px' : cardWidth < 160 ? '52px' : '60px';
            clearTimeout( timer );
        grid.style.gridAutoRows = rowHeight;
            var query = $input.val();
        cards.forEach( function ( c ) {
            /* Debounce de 250 ms pour ne pas spammer sur chaque frappe */
             c.style.height = rowHeight;
            timer = setTimeout( function () {
                doSearch( query );
             }, 250 );
         } );
         } );


         grid.classList.add( 'is-ready' );
         $next.on( 'click', function () {
    }
            goTo( currentIndex + 1 );
        } );


    function adjustAllGrids() {
        $prev.on( 'click', function () {
        document.querySelectorAll( '.minicard-grid' ).forEach( adjustGrid );
            goTo( currentIndex - 1 );
    }
        } );


    /* Exécution initiale */
        $clear.on( 'click', function () {
    if ( document.readyState === 'loading' ) {
            $input.val( '' ).trigger( 'focus' );
        document.addEventListener( 'DOMContentLoaded', adjustAllGrids );
            clearHighlights();
    } else {
        } );
        adjustAllGrids();
    }


    /* Resize fenêtre */
        /* Raccourcis clavier dans l'input */
    var resizeTimer;
        $input.on( 'keydown', function ( e ) {
    window.addEventListener( 'resize', function () {
            if ( e.key === 'Enter' ) {
        clearTimeout( resizeTimer );
                e.preventDefault();
        resizeTimer = setTimeout( adjustAllGrids, 80 );
                if ( e.shiftKey ) {
    } );
                    goTo( currentIndex - 1 );
 
                } else {
    /* Observer les changements de classe sur :root (changement de mode Citizen) */
                    goTo( currentIndex + 1 );
    var rootObserver = new MutationObserver( function ( mutations ) {
                }
        mutations.forEach( function ( mutation ) {
            }
             if ( mutation.attributeName === 'class' ) {
             if ( e.key === 'Escape' ) {
                 /* Petit délai pour laisser Citizen appliquer ses styles */
                 $input.val( '' );
                 setTimeout( adjustAllGrids, 150 );
                clearHighlights();
                 $input.trigger( 'blur' );
             }
             }
         } );
         } );
     } );
     } );
 
} );
    rootObserver.observe( document.documentElement, {
        attributes: true,
        attributeFilter: [ 'class' ]
    } );
 
}() );

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">&#x2315;</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">&#x2191;</button>' +
                '<button class="nefald-page-search__next" title="Résultat suivant">&#x2193;</button>' +
                '<button class="nefald-page-search__clear" title="Effacer">&#x2715;</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' );
            }
        } );
    } );
} );
Les témoins (''cookies'') nous aident à fournir nos services. En utilisant nos services, vous acceptez notre utilisation de témoins.