NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Rexel price
// @namespace http://tampermonkey.net/
// @version 0.2.2
// @description Affiche les détails du prix sans montrer le pop-up
// @author Anubys
// @copyright 2026, Anubys
// @license MIT
// @match https://www.rexel.fr/frx/*
// @updateURL https://openuserjs.org/meta/Anubys/Rexel_price.meta.js
// @downloadURL https://openuserjs.org/install/Anubys/Rexel_price.user.js
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const LOG_PREFIX = '[Rexel price]';
const RESULT_ID = 'rexel-price-details';
const HIDDEN_STYLE_ID = 'rexel-hidden-popover-style';
const TIMEOUT = 15000;
console.log(`${LOG_PREFIX} Script chargé.`);
// ============================================================
// OUTILS
// ============================================================
function cleanText(value) {
return String(value || '')
.replace(/\u00a0/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function waitForCondition(
callback,
timeout = TIMEOUT,
description = 'Condition'
) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
function check() {
let result = null;
try {
result = callback();
} catch (error) {
console.debug(
`${LOG_PREFIX} Attente ${description} :`,
error
);
}
if (result) {
resolve(result);
return;
}
if (Date.now() - startTime >= timeout) {
reject(
new Error(
`${description} non satisfaite après ${timeout} ms.`
)
);
return;
}
setTimeout(check, 100);
}
check();
});
}
// ============================================================
// RECHERCHE DU BOUTON TARIFAIRE
// ============================================================
function findPricingTrigger() {
const exactSelector =
'i.fas.fa-info-circle.fa-sm.ml-1.ds-mt-2.popover-icon[role="button"]';
const exactMatches = Array.from(
document.querySelectorAll(exactSelector)
);
if (!exactMatches.length) {
console.debug(
`${LOG_PREFIX} Bouton exact pas encore présent.`,
exactSelector
);
return null;
}
console.log(
`${LOG_PREFIX} Bouton tarifaire exact trouvé :`,
exactMatches.length,
exactMatches
);
/*
* Priorité au bouton situé dans le vrai composant tarifaire.
*/
for (const candidate of exactMatches) {
const component = candidate.closest(
'mf-pdp-price-info-popover'
);
if (component) {
console.log(
`${LOG_PREFIX} Bouton tarifaire retenu :`,
candidate,
'Composant :',
component
);
return candidate;
}
}
/*
* Fallback si Rexel modifie légèrement le composant.
*/
for (const candidate of exactMatches) {
let parent = candidate.parentElement;
for (let level = 0; parent && level < 8; level++) {
const text = cleanText(
parent.textContent
).toLowerCase();
if (
text.includes('€') ||
text.includes('prix') ||
text.includes('ht') ||
text.includes('ttc') ||
text.includes('remise')
) {
console.log(
`${LOG_PREFIX} Bouton tarifaire fallback retenu :`,
candidate
);
return candidate;
}
parent = parent.parentElement;
}
}
return exactMatches[0];
}
// ============================================================
// MASQUAGE TEMPORAIRE DU POP-UP
// ============================================================
function hidePopoversBeforeOpening() {
if (!document.getElementById(HIDDEN_STYLE_ID)) {
const style = document.createElement('style');
style.id = HIDDEN_STYLE_ID;
style.textContent = `
body.rexel-price-reading
mf-pdp-price-info-popover ds-popover {
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
body.rexel-price-reading
mf-pdp-price-info-popover ds-elevation.ds-popover {
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
`;
document.head.appendChild(style);
}
document.body.classList.add(
'rexel-price-reading'
);
}
function removePopoverHidingMode() {
document.body.classList.remove(
'rexel-price-reading'
);
}
// ============================================================
// RECHERCHE DU POP-UP TARIFAIRE
// ============================================================
function findPricingPopover(trigger) {
if (!trigger) {
return null;
}
const component = trigger.closest(
'mf-pdp-price-info-popover'
);
if (!component) {
console.debug(
`${LOG_PREFIX} Composant mf-pdp-price-info-popover introuvable.`
);
return null;
}
const popover = component.querySelector(
'ds-popover ds-elevation.ds-popover'
);
if (!popover) {
console.debug(
`${LOG_PREFIX} Popover tarifaire pas encore créé.`
);
return null;
}
const text = cleanText(
popover.textContent
);
const textLower = text.toLowerCase();
const hasPricingContent =
textLower.includes('prix de base') ||
textLower.includes('mon prix') ||
textLower.includes('prix net') ||
textLower.includes('décomposition du tarif') ||
textLower.includes('remise');
if (!hasPricingContent) {
console.debug(
`${LOG_PREFIX} Popover trouvé mais non reconnu comme tarifaire :`,
text
);
return null;
}
console.log(
`${LOG_PREFIX} Popover tarifaire détecté :`,
popover,
'Texte :',
text
);
return popover;
}
// ============================================================
// EXTRACTION DES PRIX
// ============================================================
function getValueByLabel(
popoverBody,
expectedLabels
) {
const labels = expectedLabels.map(
label => cleanText(label).toLowerCase()
);
const rows = popoverBody.querySelectorAll(
'.d-flex.justify-content-between'
);
for (const row of rows) {
const children = Array.from(
row.children
);
if (children.length < 2) {
continue;
}
const label = cleanText(
children[0].textContent
).toLowerCase();
const matches = labels.some(
expectedLabel =>
label.includes(expectedLabel)
);
if (!matches) {
continue;
}
return cleanText(
children[
children.length - 1
].textContent
);
}
/*
* Fallback plus souple.
*/
const allElements =
popoverBody.querySelectorAll('*');
for (const element of allElements) {
const text = cleanText(
element.textContent
).toLowerCase();
const matchedLabel = labels.find(
expectedLabel =>
text === expectedLabel ||
text.startsWith(
expectedLabel + ' '
)
);
if (!matchedLabel) {
continue;
}
const parent = element.parentElement;
if (!parent) {
continue;
}
const siblings =
Array.from(parent.children);
if (siblings.length < 2) {
continue;
}
const value = cleanText(
siblings[
siblings.length - 1
].textContent
);
if (
value &&
value.toLowerCase() !== matchedLabel
) {
return value;
}
}
return '';
}
function extractPrices(popoverBody) {
return {
basePrice: getValueByLabel(
popoverBody,
[
'Prix de base HT',
'Prix de base'
]
),
discount: getValueByLabel(
popoverBody,
[
'Remise'
]
),
netPrice: getValueByLabel(
popoverBody,
[
'Mon prix',
'Prix net HT',
'Prix net'
]
)
};
}
// ============================================================
// RECHERCHE DE LA ZONE D'AFFICHAGE
// ============================================================
function findDisplayContainer(trigger) {
let parent = trigger.parentElement;
for (
let level = 0;
parent && level < 8;
level++
) {
const text = cleanText(
parent.textContent
).toLowerCase();
if (
text.includes('€') ||
text.includes('prix') ||
text.includes('ht')
) {
return parent;
}
parent = parent.parentElement;
}
const selectors = [
'.rex-product-price',
'[class*="product-price"]',
'[class*="price-container"]',
'[class*="price-information"]',
'[class*="price-block"]'
];
for (const selector of selectors) {
const container =
document.querySelector(selector);
if (container) {
return container;
}
}
return trigger.parentElement;
}
// ============================================================
// AFFICHAGE
// ============================================================
function createRow(
label,
value,
emphasized = false
) {
const row =
document.createElement('tr');
const labelCell =
document.createElement('td');
const valueCell =
document.createElement('td');
labelCell.textContent =
`${label} :`;
valueCell.textContent =
value || 'Non disponible';
Object.assign(
labelCell.style,
{
padding: '4px 10px 4px 0',
whiteSpace: 'nowrap'
}
);
Object.assign(
valueCell.style,
{
padding: '4px 0',
textAlign: 'right',
whiteSpace: 'nowrap',
fontWeight:
emphasized
? '700'
: '400'
}
);
row.append(
labelCell,
valueCell
);
return row;
}
function displayPrices(
prices,
trigger
) {
const container =
findDisplayContainer(trigger);
if (!container) {
throw new Error(
'Zone d’affichage du prix introuvable.'
);
}
document
.getElementById(RESULT_ID)
?.remove();
const wrapper =
document.createElement('div');
wrapper.id = RESULT_ID;
Object.assign(
wrapper.style,
{
display: 'block',
width: '260px',
marginTop: '12px',
marginLeft: 'auto',
padding: '10px',
border: '1px solid #444',
borderRadius: '4px',
boxSizing: 'border-box',
backgroundColor: '#ffffff',
color: '#222222',
fontSize: '14px',
lineHeight: '1.4'
}
);
const table =
document.createElement('table');
Object.assign(
table.style,
{
width: '100%',
borderCollapse: 'collapse'
}
);
table.append(
createRow(
'Prix de base HT',
prices.basePrice
),
createRow(
'Remise',
prices.discount
),
createRow(
'Prix net HT',
prices.netPrice,
true
)
);
wrapper.appendChild(table);
container.appendChild(wrapper);
}
// ============================================================
// FERMETURE DU POP-UP
// ============================================================
async function closePopover(
trigger,
popoverBody
) {
const component = trigger?.closest(
'mf-pdp-price-info-popover'
);
if (!component) {
removePopoverHidingMode();
return;
}
let popover =
popoverBody ||
component.querySelector(
'ds-popover ds-elevation.ds-popover'
);
if (!popover) {
removePopoverHidingMode();
return;
}
console.log(
`${LOG_PREFIX} Fermeture du popover tarifaire.`,
popover
);
/*
* Première tentative :
* simuler un véritable clic extérieur.
*/
const outsideTarget =
document.querySelector('h1') ||
document.querySelector('main') ||
document.body;
const eventOptions = {
bubbles: true,
cancelable: true,
view: window
};
try {
outsideTarget.dispatchEvent(
new PointerEvent(
'pointerdown',
eventOptions
)
);
} catch (error) {
console.debug(
`${LOG_PREFIX} PointerEvent non disponible :`,
error
);
}
outsideTarget.dispatchEvent(
new MouseEvent(
'mousedown',
eventOptions
)
);
outsideTarget.dispatchEvent(
new MouseEvent(
'mouseup',
eventOptions
)
);
outsideTarget.dispatchEvent(
new MouseEvent(
'click',
eventOptions
)
);
/*
* On laisse Angular traiter le clic extérieur.
*/
await new Promise(
resolve =>
setTimeout(
resolve,
200
)
);
/*
* On vérifie si le panneau est toujours présent.
*/
popover = component.querySelector(
'ds-popover ds-elevation.ds-popover'
);
if (popover) {
console.log(
`${LOG_PREFIX} Popover toujours présent après clic extérieur : suppression forcée.`
);
/*
* Fallback garanti :
* suppression du panneau généré.
*/
popover.remove();
}
/*
* On laisse un très court délai avant de retirer
* le mode de masquage pour éviter un flash visuel.
*/
await new Promise(
resolve =>
setTimeout(
resolve,
50
)
);
removePopoverHidingMode();
console.log(
`${LOG_PREFIX} Popover tarifaire fermé.`
);
}
// ============================================================
// CHATBOT
// ============================================================
function closeChatbotIfOpen() {
const chatbotWindow =
document.querySelector(
'.chatbot-window'
);
if (!chatbotWindow) {
return false;
}
let closeButton =
chatbotWindow.querySelector(
'.chatbot-header__actions--right ds-icon-button button'
);
if (!closeButton) {
closeButton = Array.from(
chatbotWindow.querySelectorAll(
'button'
)
).find(button => {
const icon =
button.querySelector(
'.ds-icon'
);
return cleanText(
icon?.textContent
).toLowerCase() ===
'expand_more';
});
}
if (!closeButton) {
console.debug(
`${LOG_PREFIX} Chatbot ouvert mais bouton de fermeture non trouvé.`,
chatbotWindow
);
return false;
}
console.log(
`${LOG_PREFIX} Chatbot ouvert : fermeture.`,
closeButton
);
closeButton.click();
return true;
}
async function closeChatbotOnceWhenItAppears(
timeout = 3000
) {
const startTime =
Date.now();
while (
Date.now() - startTime <
timeout
) {
if (closeChatbotIfOpen()) {
return true;
}
await new Promise(
resolve =>
setTimeout(
resolve,
100
)
);
}
console.debug(
`${LOG_PREFIX} Aucun chatbot ouvert pendant la fenêtre de fermeture.`
);
return false;
}
// ============================================================
// PROGRAMME PRINCIPAL
// ============================================================
async function run() {
let trigger = null;
let popoverBody = null;
try {
trigger =
await waitForCondition(
findPricingTrigger,
TIMEOUT,
'Bouton de détail du prix introuvable'
);
console.log(
`${LOG_PREFIX} Bouton du prix trouvé.`,
trigger
);
/*
* Masquage installé AVANT le clic pour éviter
* l'affichage visuel du pop-up.
*/
hidePopoversBeforeOpening();
/*
* Ouverture du pop-up.
*/
trigger.click();
/*
* On cherche uniquement le pop-up rattaché
* au composant du bouton tarifaire.
*/
popoverBody =
await waitForCondition(
() =>
findPricingPopover(
trigger
),
TIMEOUT,
'Popover tarifaire introuvable'
);
/*
* Extraction des valeurs.
*/
const prices =
extractPrices(
popoverBody
);
console.log(
`${LOG_PREFIX} Valeurs extraites :`,
prices
);
if (
!prices.basePrice &&
!prices.discount &&
!prices.netPrice
) {
throw new Error(
'Aucune donnée tarifaire trouvée dans le pop-up.'
);
}
/*
* Gestion éventuelle du chatbot.
*/
await closeChatbotOnceWhenItAppears(
3000
);
/*
* Affichage des informations tarifaires.
*/
displayPrices(
prices,
trigger
);
/*
* Fermeture du pop-up.
*/
await closePopover(
trigger,
popoverBody
);
} catch (error) {
console.error(
`${LOG_PREFIX} Échec du script :`,
error
);
/*
* On retire toujours le masquage CSS.
*/
removePopoverHidingMode();
/*
* Nettoyage de secours du pop-up tarifaire.
*/
if (trigger) {
const component =
trigger.closest(
'mf-pdp-price-info-popover'
);
const popover =
component?.querySelector(
'ds-popover ds-elevation.ds-popover'
);
if (popover) {
console.warn(
`${LOG_PREFIX} Nettoyage du popover après erreur.`
);
popover.remove();
}
}
}
}
run();
})();