« MediaWiki:Common.js » : différence entre les versions
Page de l’interface de MediaWiki
Autres actions
Aucun résumé des modifications Balise : Révoqué |
Aucun résumé des modifications |
||
| (19 versions intermédiaires par le même utilisateur non affichées) | |||
| Ligne 15 : | Ligne 15 : | ||
/** | /** | ||
* | * MinecraftConnect - Boutons de copie d'adresse serveur Minecraft | ||
* Inspiré de l'extension PreToClip | |||
*/ | */ | ||
(function() { | (function() { | ||
'use strict'; | |||
if (!$ | 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(); | 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' ); | |||
} | |||
} ); | |||
} ); | |||
} ); | |||