Hello,
Je ne suis pas connaisseur de ce que tu demandes:
Prompt : Peux-tu me réaliser avec les commentaires un script javascript qui afficherait en miniature les images postées dans un thread au survol du titre de celui-ci
Ce script serait par la suite chargé et lancé par Tampermonkey
voicie le résultat à tester et contrôler ... amélioré (1er jet)
Installation
Installe l'extension Tampermonkey dans ton navigateur
Crée un nouveau script et colle le contenu du fichier
Sauvegarde → le script est actif immédiatement
Adaptation au forum cible
La seule chose à modifier est dans le bloc
CONFIG en haut du script :
threadTitleSelector — le sélecteur CSS des liens de titre. Voici les valeurs courantes :
Forum Sélecteur
phpBB 'a.topictitle'
vBulletin 'a.title'
XenForo 'a.PreviewTooltip'
SMF 'span.topictitle a'
Discourse 'a.title.raw-link'Adaptation au forum cible
La seule chose à modifier est dans le bloc CONFIG en haut du script :
threadTitleSelector — le sélecteur CSS des liens de titre. Voici les valeurs courantes :
Pour trouver le bon sélecteur sur n'importe quel forum : clic droit sur un titre → Inspecter, puis repérer la classe du <a>.
De même, dans la fonction extractImages(), le sélecteur .postbody, .postcontent cible les zones de contenu des posts — à adapter si les images ne sont pas détectées.
Ce que fait le script
- Délai configurable avant l'affichage pour éviter les déclenchements intempestifs
- Cache en mémoire : un thread déjà survolé ne refait pas de requête réseau
- Filtre anti-smileys/avatars : exclut les petites images et celles dont l'URL contient avatar, icon, etc.
- Repositionnement intelligent : le tooltip ne déborde jamais hors de l'écran
- MutationObserver : fonctionne même sur les forums avec chargement AJAX/infinite scroll
- Annulation XHR : si tu quittes le survol avant la fin du chargement, la requête est annulée proprement
Code : Tout sélectionner
// ==UserScript==
// @name Thread Image Preview on Hover
// @namespace https://github.com/francois/thread-image-preview
// @version 1.0.0
// @description Affiche en miniature les images d'un thread au survol de son titre
// @author François A
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect *
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ============================================================
// ██████╗ ██████╗ ███╗ ██╗███████╗██╗ ██████╗
// ██╔════╝██╔═══██╗████╗ ██║██╔════╝██║██╔════╝
// ██║ ██║ ██║██╔██╗ ██║█████╗ ██║██║ ███╗
// ██║ ██║ ██║██║╚██╗██║██╔══╝ ██║██║ ██║
// ╚██████╗╚██████╔╝██║ ╚████║██║ ██║╚██████╔╝
// ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝
// ============================================================
// Adapte ces paramètres à la structure HTML du forum cible.
// ============================================================
const CONFIG = {
// ─── Sélecteurs CSS ────────────────────────────────────────
// Sélecteur des liens de titre de thread dans la liste des topics
// Exemples courants :
// phpBB → 'a.topictitle'
// vBulletin → 'a.title'
// XenForo → 'a.PreviewTooltip, a[data-preview-url]'
// SMF → 'span.topictitle a'
threadTitleSelector: 'a.topictitle, a.thread-title, a[data-thread-id]',
// ─── Comportement du tooltip ───────────────────────────────
maxThumbnails: 6, // Nombre maximum de miniatures à afficher
thumbnailSize: 120, // Taille (px) de chaque miniature (carré)
tooltipDelay: 300, // Délai (ms) avant l'apparition du tooltip
tooltipMaxWidth: 520, // Largeur max (px) du tooltip
tooltipOffset: { x: 15, y: 10 }, // Décalage (px) par rapport au curseur
// ─── Réseau ────────────────────────────────────────────────
fetchTimeout: 8000, // Timeout (ms) pour la requête XHR du thread
cacheEnabled: true, // Mise en cache des résultats pour éviter les requêtes répétées
};
// ============================================================
// CACHE — Stocke les URLs d'images déjà récupérées
// Clé : URL du thread
// Valeur: tableau d'URLs d'images (ou tableau vide si aucune)
// ============================================================
const imageCache = new Map();
// ============================================================
// TOOLTIP — Élément DOM unique réutilisé pour tous les survols
// ============================================================
let tooltipEl = null; // Référence au tooltip dans le DOM
let hoverTimer = null; // Timer pour le délai d'affichage
let currentRequest = null; // Requête XHR en cours (pour annulation)
// ─────────────────────────────────────────────────────────────
// INJECTION CSS — Styles du tooltip et des miniatures
// ─────────────────────────────────────────────────────────────
GM_addStyle(`
/* Conteneur principal du tooltip */
#tip-image-preview {
position: fixed;
z-index: 999999;
background: #1e1e2e;
border: 1px solid #444466;
border-radius: 8px;
padding: 10px;
box-shadow: 0 8px 32px rgba(0,0,0,0.55);
max-width: ${CONFIG.tooltipMaxWidth}px;
display: flex;
flex-wrap: wrap;
gap: 8px;
pointer-events: none; /* Ne bloque pas les clics sous le tooltip */
opacity: 0;
transform: translateY(6px);
transition: opacity 0.18s ease, transform 0.18s ease;
}
/* Classe ajoutée pour rendre le tooltip visible */
#tip-image-preview.tip-visible {
opacity: 1;
transform: translateY(0);
}
/* Chaque vignette image */
#tip-image-preview .tip-thumb {
width: ${CONFIG.thumbnailSize}px;
height: ${CONFIG.thumbnailSize}px;
object-fit: cover;
border-radius: 5px;
border: 2px solid #33334d;
background: #2a2a3e;
display: block;
}
/* Message affiché pendant le chargement ou si aucune image */
#tip-image-preview .tip-msg {
color: #aaaacc;
font-size: 13px;
font-family: sans-serif;
padding: 6px 4px;
white-space: nowrap;
}
/* Compteur "+N autres" affiché si les images dépassent maxThumbnails */
#tip-image-preview .tip-more {
color: #7777aa;
font-size: 12px;
font-family: sans-serif;
align-self: flex-end;
padding: 2px 4px;
}
`);
// ─────────────────────────────────────────────────────────────
// createTooltip()
// Crée l'élément tooltip unique et l'insère dans le <body>.
// Appelé une seule fois au démarrage du script.
// ─────────────────────────────────────────────────────────────
function createTooltip() {
const el = document.createElement('div');
el.id = 'tip-image-preview';
document.body.appendChild(el);
return el;
}
// ─────────────────────────────────────────────────────────────
// showTooltip(content)
// Remplace le contenu du tooltip et le rend visible.
// @param {string} htmlContent — HTML à injecter dans le tooltip
// ─────────────────────────────────────────────────────────────
function showTooltip(htmlContent) {
tooltipEl.innerHTML = htmlContent;
tooltipEl.classList.add('tip-visible');
}
// ─────────────────────────────────────────────────────────────
// hideTooltip()
// Masque le tooltip et annule tout timer/requête en cours.
// ─────────────────────────────────────────────────────────────
function hideTooltip() {
clearTimeout(hoverTimer);
// Annule la requête réseau si elle est toujours en vol
if (currentRequest) {
try { currentRequest.abort(); } catch (_) {}
currentRequest = null;
}
tooltipEl.classList.remove('tip-visible');
}
// ─────────────────────────────────────────────────────────────
// positionTooltip(mouseX, mouseY)
// Repositionne le tooltip en suivant le curseur.
// Gère les débordements sur les bords droits/bas de l'écran.
// ─────────────────────────────────────────────────────────────
function positionTooltip(mouseX, mouseY) {
const vw = window.innerWidth;
const vh = window.innerHeight;
const tw = tooltipEl.offsetWidth || CONFIG.tooltipMaxWidth;
const th = tooltipEl.offsetHeight || 160;
let left = mouseX + CONFIG.tooltipOffset.x;
let top = mouseY + CONFIG.tooltipOffset.y;
// Débordement à droite → coller à droite de l'écran avec marge
if (left + tw > vw - 10) left = vw - tw - 10;
// Débordement en bas → afficher au-dessus du curseur
if (top + th > vh - 10) top = mouseY - th - CONFIG.tooltipOffset.y;
tooltipEl.style.left = `${left}px`;
tooltipEl.style.top = `${top}px`;
}
// ─────────────────────────────────────────────────────────────
// extractImages(htmlString)
// Parse le HTML d'une page de thread et retourne les URLs
// des images trouvées dans les messages/posts.
// @param {string} htmlString — HTML brut du thread
// @return {string[]} — Tableau d'URLs d'images
// ─────────────────────────────────────────────────────────────
function extractImages(htmlString) {
// On crée un document virtuel pour parser proprement le HTML
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
// ── Sélecteurs des zones de contenu des posts ──────────────
// Adapter selon le forum :
// phpBB → '.postbody'
// vBulletin → '.postcontent'
// XenForo → '.message-body'
// SMF → '.post'
const postBodies = doc.querySelectorAll(
'.postbody, .postcontent, .message-body, .message-userContent, .post, .post-content'
);
const urls = [];
// Si des zones de posts sont trouvées, on cherche les images dedans
const searchScope = postBodies.length > 0
? Array.from(postBodies)
: [doc.body]; // Fallback : on cherche dans tout le body
for (const scope of searchScope) {
const imgs = scope.querySelectorAll('img');
for (const img of imgs) {
const src = img.src || img.dataset.src || img.dataset.lazySrc || '';
// ── Filtres : on exclut les éléments d'interface ──────
if (!src) continue; // Pas de source
if (src.startsWith('data:')) continue; // Images base64 (smileys, etc.)
if (/\.(svg)(\?|$)/i.test(src)) continue; // Fichiers SVG (icônes)
// Exclure les petites images qui sont probablement des smileys/avatars
const w = parseInt(img.width || img.getAttribute('width') || 0);
const h = parseInt(img.height || img.getAttribute('height') || 0);
if ((w > 0 && w < 60) || (h > 0 && h < 60)) continue;
// Exclure les images dont l'URL contient des mots-clés d'interface
if (/avatar|smiley|emoji|icon|logo|banner|rank|button/i.test(src)) continue;
urls.push(src);
}
}
// Déduplique les URLs
return [...new Set(urls)];
}
// ─────────────────────────────────────────────────────────────
// fetchAndPreview(threadUrl, mouseX, mouseY)
// Récupère le HTML du thread via GM_xmlhttpRequest, extrait
// les images et affiche le tooltip avec les miniatures.
// Utilise le cache si disponible.
// ─────────────────────────────────────────────────────────────
function fetchAndPreview(threadUrl, mouseX, mouseY) {
// ── Affichage immédiat d'un indicateur de chargement ──────
showTooltip('<span class="tip-msg">⏳ Chargement des images…</span>');
positionTooltip(mouseX, mouseY);
// ── Vérification du cache ─────────────────────────────────
if (CONFIG.cacheEnabled && imageCache.has(threadUrl)) {
renderImages(imageCache.get(threadUrl));
return;
}
// ── Requête XHR cross-origin via GM_xmlhttpRequest ────────
// Nécessite @grant GM_xmlhttpRequest et @connect * dans le header
currentRequest = GM_xmlhttpRequest({
method: 'GET',
url: threadUrl,
timeout: CONFIG.fetchTimeout,
onload(response) {
currentRequest = null;
if (response.status !== 200) {
showTooltip('<span class="tip-msg">⚠️ Impossible de charger le thread.</span>');
return;
}
const images = extractImages(response.responseText);
// Mise en cache du résultat
if (CONFIG.cacheEnabled) {
imageCache.set(threadUrl, images);
}
renderImages(images);
},
onerror() {
currentRequest = null;
showTooltip('<span class="tip-msg">❌ Erreur réseau.</span>');
},
ontimeout() {
currentRequest = null;
showTooltip('<span class="tip-msg">⏱️ Délai d\'attente dépassé.</span>');
},
});
}
// ─────────────────────────────────────────────────────────────
// renderImages(imageUrls)
// Construit le HTML du tooltip à partir d'un tableau d'URLs
// et met à jour l'affichage.
// @param {string[]} imageUrls — URLs des images à afficher
// ─────────────────────────────────────────────────────────────
function renderImages(imageUrls) {
if (!imageUrls || imageUrls.length === 0) {
showTooltip('<span class="tip-msg">🖼️ Aucune image dans ce thread.</span>');
return;
}
const displayed = imageUrls.slice(0, CONFIG.maxThumbnails);
const remaining = imageUrls.length - displayed.length;
// Génère une balise <img> pour chaque miniature
// onerror : si l'image échoue à charger, on la masque
let html = displayed.map(url =>
`<img class="tip-thumb" src="${escapeHtml(url)}" loading="lazy"
alt="aperçu" onerror="this.style.display='none'">`
).join('');
// Affiche un compteur si toutes les images ne tiennent pas
if (remaining > 0) {
html += `<span class="tip-more">+${remaining} autre${remaining > 1 ? 's' : ''}</span>`;
}
showTooltip(html);
}
// ─────────────────────────────────────────────────────────────
// escapeHtml(str)
// Échappe les caractères HTML dangereux pour éviter les
// injections XSS lors de l'insertion dans innerHTML.
// @param {string} str
// @return {string}
// ─────────────────────────────────────────────────────────────
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/</g, '<')
.replace(/>/g, '>');
}
// ─────────────────────────────────────────────────────────────
// attachHoverListeners()
// Observe le DOM pour détecter les titres de threads
// (y compris ceux chargés dynamiquement via AJAX/infinite scroll)
// et attache les événements mouseenter / mouseleave / mousemove.
// ─────────────────────────────────────────────────────────────
function attachHoverListeners() {
// Fonction qui traite tous les titres de thread présents dans un nœud
function processLinks(root) {
const links = root.querySelectorAll
? root.querySelectorAll(CONFIG.threadTitleSelector)
: [];
for (const link of links) {
// Évite de doubler les écouteurs avec un flag sur l'élément
if (link.dataset.tipBound) continue;
link.dataset.tipBound = '1';
// ── mouseenter : déclenche le timer de chargement ─────
link.addEventListener('mouseenter', function (e) {
const href = this.href;
if (!href || href === '#') return;
hoverTimer = setTimeout(() => {
fetchAndPreview(href, e.clientX, e.clientY);
}, CONFIG.tooltipDelay);
});
// ── mouseleave : annule tout et cache le tooltip ──────
link.addEventListener('mouseleave', function () {
hideTooltip();
});
// ── mousemove : repositionne le tooltip en temps réel ─
link.addEventListener('mousemove', function (e) {
if (tooltipEl.classList.contains('tip-visible')) {
positionTooltip(e.clientX, e.clientY);
}
});
}
}
// Premier passage sur la page déjà chargée
processLinks(document);
// ── MutationObserver : gère le contenu chargé dynamiquement ─
// Utile pour les forums avec pagination AJAX ou infinite scroll
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
processLinks(node);
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
// ─────────────────────────────────────────────────────────────
// INIT — Point d'entrée du script
// ─────────────────────────────────────────────────────────────
function init() {
tooltipEl = createTooltip();
attachHoverListeners();
console.info('[Thread Image Preview] Script initialisé ✔');
}
// Démarre le script une fois que le DOM est disponible
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();