Compare commits

...

1 Commits

5 changed files with 387 additions and 44 deletions
+30 -14
View File
@@ -211,23 +211,31 @@ async function fetchCurrentUser(origin, phpsessid) {
const html = await resp.text();
if (looksLikeLoginPage(html)) return null;
// Essais de patterns (du plus spécifique au plus générique)
// v4.2.2 : patterns spécifiques à la structure EasyVista réelle du Canton
// de Vaud (identifiés à partir du HTML de la page d'accueil). L'user est
// affiché dans un dropdown ".ev-employee-dropdown" avec ces éléments :
// <span class="profile-info">
// <span class="h5" title="Nom, Prénom">Nom, Prénom</span>
// <span class="h6" title="3.3 DGNSI-ServiceDesk">3.3 DGNSI-ServiceDesk</span>
// ...
// </span>
// Le title du <a> parent contient aussi "Nom, Prénom / Service / Société".
const patterns = [
// Attribut data-user-name (si EasyVista l'expose)
// 1) Le plus fiable : span class="h5" dans profile-info (structure EV 2026)
/<span\s+class=["']profile-info["'][^>]*>\s*<span\s+class=["']h5["'][^>]*title=["']([^"']{2,80})["']/i,
// 2) Fallback : span class="h5" avec title= même hors profile-info
/<span\s+class=["']h5["'][^>]*title=["']([^"']{2,80})["'][^>]*>\s*([^<]{2,80})<\/span>/i,
// 3) Fallback : title= de ev-employee-dropdown (format "Nom, Prénom / Service / Société")
/class=["'][^"']*ev-employee-dropdown[^"']*["'][^>]*title=["']([^"'\/]+?)(?:\s*\/\s*[^"']+)?["']/i,
// 4) Anciens patterns génériques (autres instances EasyVista éventuelles)
/data-user-name\s*=\s*["']([^"']+)["']/i,
/data-username\s*=\s*["']([^"']+)["']/i,
/data-user-fullname\s*=\s*["']([^"']+)["']/i,
// Variable JS typique EasyVista
/EV\.User\.name\s*=\s*["']([^"']+)["']/,
/EV\.User\.fullname\s*=\s*["']([^"']+)["']/,
/userFullName\s*[:=]\s*["']([^"']+)["']/,
/currentUser(?:Name)?\s*[:=]\s*["']([^"']+)["']/,
// Balises cachées ou spans avec classe "user"
/<(?:span|div)[^>]*class=["'][^"']*(?:user[_-]?(?:name|full|display))[^"']*["'][^>]*>([^<]{2,80})<\/(?:span|div)>/i,
// "Bienvenue" / "Welcome"
/(?:Bienvenue|Welcome)[,\s]+(?:M\.?\s+|Mme\s+)?([A-ZÀÁÂÄÇÉÈÊËÍÎÏÓÔÖÚÛÜÑ][\wÀ-ÿ'.\-]+(?:\s*,?\s+[A-ZÀÁÂÄÇÉÈÊËÍÎÏÓÔÖÚÛÜÑ][\wÀ-ÿ'.\-]+){0,3})/,
// Title de la page (souvent "EasyVista - Nom Prénom")
/<title>([^<]*)<\/title>/i
// 5) "Bienvenue" / "Welcome"
/(?:Bienvenue|Welcome)[,\s]+(?:M\.?\s+|Mme\s+)?([A-ZÀÁÂÄÇÉÈÊËÍÎÏÓÔÖÚÛÜÑ][\wÀ-ÿ'.\-]+(?:\s*,?\s+[A-ZÀÁÂÄÇÉÈÊËÍÎÏÓÔÖÚÛÜÑ][\wÀ-ÿ'.\-]+){0,3})/
];
let name = null;
@@ -236,7 +244,6 @@ async function fetchCurrentUser(origin, phpsessid) {
if (m && m[1]) {
const candidate = m[1].trim()
.replace(/\s+/g, " ")
// Enlever des éléments du <title> type "EasyVista" / "Planning" / etc.
.replace(/^(?:EasyVista|EV|Accueil|Home|Planning|ITSMA)[\s\-|•]+/i, "")
.replace(/[\s\-|•]+(?:EasyVista|EV|ITSMA)$/i, "")
.trim();
@@ -249,7 +256,16 @@ async function fetchCurrentUser(origin, phpsessid) {
}
}
// Chercher aussi le login (ID court) — utile comme fallback secondaire
// v4.2.2 : on extrait aussi le service/unité si disponible (h6 à côté du h5)
let service = null;
const serviceMatch = html.match(
/<span\s+class=["']profile-info["'][^>]*>[\s\S]{0,500}?<span\s+class=["']h6["'][^>]*title=["']([^"']{2,80})["']/i
);
if (serviceMatch && serviceMatch[1]) {
service = serviceMatch[1].trim();
}
// Login / identifiant court (optionnel)
let login = null;
const loginPatterns = [
/data-user-login\s*=\s*["']([^"']+)["']/i,
@@ -265,8 +281,8 @@ async function fetchCurrentUser(origin, phpsessid) {
}
}
if (!name && !login) return null;
return { name, login };
if (!name && !login && !service) return null;
return { name, login, service };
}
// ============================================================================
+2 -2
View File
@@ -1,8 +1,8 @@
{
"manifest_version": 3,
"name": "Planning Techniciens — Vue claire",
"version": "4.2.1",
"description": "Vue claire du planning EasyVista (itsma.etat-de-vaud.ch et itsma.vd.ch). v4.2.1 : messages d'erreur clairs (session expirée vs EasyVista inaccessible) avec bouton Ouvrir EasyVista et Réessayer, vouvoiement uniformisé. Inclut v4.2.0 : contact + personne de contact sur site avec anomalie rouge, parser téléphone élargi (41XXX sans +), sélection texte dans la bulle sans épingler, utilisateur EV connecté en haut, suppression auto-refresh 12h/15h.",
"version": "4.2.3",
"description": "Vue claire du planning EasyVista (itsma.etat-de-vaud.ch et itsma.vd.ch). v4.2.3 : titre renommé 'Planification', pastille d'initiales utilisateur à gauche (clic = popup nom complet), timeline petite popup qui suit la souris, clic timeline = grande popup persistante sous la timeline, double-clic = ouvre fiche, Ctrl+clic = fiche en arrière-plan, 2 contacts séparés par 'et' affichés sur 2 lignes, numéros courts 5 chiffres commençant par 6/7/8 avec espaces reconnus.",
"permissions": [
"activeTab",
"scripting",
+63
View File
@@ -1453,3 +1453,66 @@ html, body {
.current-user.hidden {
display: none;
}
/* ─────────────────────────────────────────────────────────────────────────
v4.2.3 : pastille utilisateur (initiales) dans la topbar gauche
───────────────────────────────────────────────────────────────────────── */
.user-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
padding: 0;
margin-right: 10px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
color: #fff;
background: var(--user-badge-color, #5b6372);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 50%;
cursor: pointer;
transition: transform 0.1s, box-shadow 0.12s;
flex-shrink: 0;
user-select: none;
}
.user-badge:hover {
transform: scale(1.06);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.user-badge:active {
transform: scale(0.97);
}
.user-badge.hidden {
display: none;
}
.user-badge.open {
/* Indication visuelle quand la popup nom est ouverte */
box-shadow: 0 0 0 2px var(--user-badge-color, #5b6372) inset,
0 0 0 2px rgba(255, 255, 255, 0.15);
}
/* Popup du nom complet, affichée juste sous la pastille au clic */
.user-name-popup {
position: fixed;
z-index: 10050;
padding: 8px 14px;
background: var(--bg, #ffffff);
color: var(--text, #111);
border: 1px solid var(--border, rgba(128, 128, 128, 0.25));
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18),
0 2px 6px rgba(0, 0, 0, 0.12);
font-size: 13px;
font-weight: 500;
white-space: nowrap;
animation: user-popup-in 0.12s ease-out;
}
.user-name-popup.hidden {
display: none;
}
@keyframes user-popup-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
+11 -3
View File
@@ -2,13 +2,18 @@
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Planning techniciens</title>
<title>Planification</title>
<link rel="stylesheet" href="viewer.css">
</head>
<body>
<header class="topbar">
<div class="topbar-left">
<h1>Planning techniciens</h1>
<!-- v4.2.3 : pastille avec initiales de l'utilisateur connecté, avant
le titre. Clic → popup fixe avec nom complet juste en dessous. -->
<button id="user-badge" class="user-badge hidden"
type="button" aria-label="Utilisateur connecté"
title="Utilisateur connecté"></button>
<h1>Planification</h1>
<div class="date-nav">
<button id="nav-prev" class="btn btn-nav" title="Jour précédent" aria-label="Jour précédent"></button>
<input type="date" id="date-picker" class="date-input">
@@ -19,7 +24,6 @@
<span id="refresh-check" class="refresh-check hidden" title="Mise à jour terminée"></span>
</div>
<div class="topbar-right">
<span id="current-user" class="current-user hidden" title="Utilisateur EasyVista connecté"></span>
<button id="refresh-partial-btn" class="btn btn-refresh" title="Actualiser : ajoute les nouvelles interventions et retire celles qui ne sont plus dans le planning. Rapide, ne re-télécharge pas les fiches déjà connues.">
<svg id="refresh-partial-icon" class="btn-refresh-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M2 8a6 6 0 0 1 10.2-4.24M14 3v3h-3" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="btn-refresh-label">Actualiser</span>
@@ -77,6 +81,10 @@
<div id="tooltip" class="tooltip hidden" role="tooltip"></div>
<!-- v4.2.3 : popup fixe du nom de l'utilisateur connecté. S'ouvre au clic
sur la pastille d'initiales (topbar gauche). -->
<div id="user-name-popup" class="user-name-popup hidden" role="dialog" aria-hidden="true"></div>
<!-- Conteneur des toasts (notifications d'ouverture) -->
<div id="toast-stack" class="toast-stack" aria-live="polite"></div>
+281 -25
View File
@@ -251,22 +251,91 @@ async function fetchAndShowCurrentUser() {
try {
const resp = await sendMessage({ type: "fetchCurrentUser" });
if (!resp || !resp.ok || !resp.user) return;
const el = document.getElementById("current-user");
if (!el) return;
const label = resp.user.name || resp.user.login || null;
if (!label) return;
el.textContent = label;
el.title = resp.user.login
? `Utilisateur EasyVista connecté : ${label} (${resp.user.login})`
: `Utilisateur EasyVista connecté : ${label}`;
el.classList.remove("hidden");
// Exposer au reste du code pour un usage éventuel plus tard
const badge = document.getElementById("user-badge");
if (!badge) return;
const fullName = resp.user.name || resp.user.login || null;
if (!fullName) return;
const initials = computeUserInitials(fullName);
badge.textContent = initials;
badge.title = fullName;
// v4.2.3 : couleur unique dérivée du nom, dans la palette neutre du thème
badge.style.setProperty("--user-badge-color", colorFromName(fullName));
badge.classList.remove("hidden");
state.currentUser = resp.user;
} catch (err) {
console.warn("[currentUser] fetch failed:", err);
}
}
// v4.2.3 : calcule les initiales depuis un nom au format "Nom, Prénom" ou
// "Nom Prénom" ou "Prénom Nom". On prend la 1re lettre majuscule de chaque
// mot/segment significatif, limité à 2 caractères.
function computeUserInitials(fullName) {
if (!fullName) return "?";
// Format "Nom, Prénom" → prendre initiale avant virgule et après
let parts;
if (fullName.includes(",")) {
parts = fullName.split(",").map(s => s.trim()).filter(Boolean);
} else {
parts = fullName.split(/\s+/).filter(Boolean);
}
const letters = parts
.map(p => p.charAt(0))
.filter(c => /[A-Za--ÿ]/.test(c))
.slice(0, 2)
.join("")
.toUpperCase();
return letters || (fullName.charAt(0).toUpperCase() || "?");
}
// v4.2.3 : couleur déterministe à partir du nom. Palette neutre et sobre
// (tons tamisés), compatible avec les thèmes clair et sombre de l'extension.
function colorFromName(name) {
// Hash simple (djb2) pour dériver un index stable
let h = 5381;
for (let i = 0; i < name.length; i++) {
h = ((h << 5) + h + name.charCodeAt(i)) & 0xffffffff;
}
const palette = [
"#5b6372", // gris bleuté
"#6b7280", // gris neutre
"#4a5568", // ardoise
"#3b5a72", // bleu profond tamisé
"#4f6a5e", // vert sauge sombre
"#6b5a4f", // brun taupe
"#5d4a6b", // prune sombre
"#6a5a3a", // kaki bronze
"#3a5a5e", // sarcelle sombre
"#6c5c67" // mauve grisé
];
return palette[Math.abs(h) % palette.length];
}
// v4.2.3 : affiche/masque la popup nom complet sous la pastille
function toggleUserNamePopup() {
const badge = document.getElementById("user-badge");
const popup = document.getElementById("user-name-popup");
if (!badge || !popup) return;
if (!popup.classList.contains("hidden")) {
hideUserNamePopup();
return;
}
if (!state.currentUser || !state.currentUser.name) return;
popup.textContent = state.currentUser.name;
popup.classList.remove("hidden");
badge.classList.add("open");
// Positionne juste en dessous de la pastille
const r = badge.getBoundingClientRect();
popup.style.left = Math.max(8, r.left) + "px";
popup.style.top = (r.bottom + 6) + "px";
}
function hideUserNamePopup() {
const popup = document.getElementById("user-name-popup");
const badge = document.getElementById("user-badge");
if (popup) popup.classList.add("hidden");
if (badge) badge.classList.remove("open");
}
// ============================================================================
// Thème clair/sombre
// ============================================================================
@@ -337,6 +406,26 @@ function bindTopbar() {
if (e.target.value) loadForDate(e.target.value);
});
// v4.2.3 : clic sur la pastille d'initiales → toggle popup nom complet
const userBadge = document.getElementById("user-badge");
if (userBadge) {
userBadge.addEventListener("click", (e) => {
e.stopPropagation();
toggleUserNamePopup();
});
}
// Clic ailleurs ou touche Escape ferme la popup
document.addEventListener("click", (e) => {
const popup = document.getElementById("user-name-popup");
if (!popup || popup.classList.contains("hidden")) return;
// Ne pas fermer si le clic est dans la popup elle-même ou sur le badge
if (e.target.closest("#user-name-popup") || e.target.closest("#user-badge")) return;
hideUserNamePopup();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") hideUserNamePopup();
});
document.getElementById("open-ev-btn").addEventListener("click", openEasyVista);
// v4.2 : écran "EasyVista inaccessible"
@@ -2322,8 +2411,152 @@ function getStatusClass(iv) {
function bindTimelinePopover(el) {
el.addEventListener("mouseenter", (e) => showTimelinePopover(e, el));
el.addEventListener("mousemove", moveTooltip);
// v4.2.3 : la petite popup timeline SUIT la souris (différent de la grande
// popup des lignes d'intervention qui est ancrée). On n'utilise pas
// moveTooltip() (no-op depuis v4.1.12) mais une fonction dédiée.
el.addEventListener("mousemove", (e) => moveTimelineTooltip(e));
el.addEventListener("mouseleave", hideTooltip);
// v4.2.3 : clic / double-clic / Ctrl+clic sur un segment timeline
// - clic simple : ferme la petite popup et ouvre la GRANDE popup
// (ancrée juste en dessous de la timeline, persistante pour permettre
// de sélectionner du texte / copier)
// - double-clic : ouvre la fiche EasyVista dans un nouvel onglet actif
// - Ctrl+clic (Cmd+clic sur Mac) : ouvre dans un onglet en arrière-plan
const kind = el.dataset.kind;
const ivIdxStr = el.dataset.ivIdx;
// Seulement sur les segments avec une intervention (pas les "hole" libres
// ni certaines absences sans ivIdx)
if (ivIdxStr === undefined) return;
let singleClickTimer = null;
el.addEventListener("click", (e) => {
// Ctrl / Cmd / molette → ouvrir fiche en arrière-plan
if (e.ctrlKey || e.metaKey || e.button === 1) {
e.preventDefault();
e.stopPropagation();
openInterventionFromTimeline(el, { background: true });
return;
}
// Clic simple (sans Ctrl) : on attend un éventuel double-clic avant
// d'ouvrir la grande popup persistante.
e.stopPropagation();
if (singleClickTimer) clearTimeout(singleClickTimer);
singleClickTimer = setTimeout(() => {
singleClickTimer = null;
openPersistentTimelinePopup(el);
}, 250);
});
el.addEventListener("dblclick", (e) => {
// Annuler le clic simple en attente
if (singleClickTimer) { clearTimeout(singleClickTimer); singleClickTimer = null; }
e.preventDefault();
e.stopPropagation();
openInterventionFromTimeline(el, { background: false });
});
}
// v4.2.3 : positionne la petite popup timeline à côté du curseur
function moveTimelineTooltip(e) {
const tip = tooltipEl();
if (!tip || !tip.classList.contains("visible")) return;
// La popup ancrée (grande bulle) ne doit pas être déplacée par la souris
if (bulleState.pinned) return;
// Si la popup affiche une grande bulle d'intervention (classe pinned-like),
// on ne la bouge pas non plus : on la laisse ancrée.
if (tip.dataset.mode === "anchored") return;
const offsetX = 14, offsetY = 16;
let x = e.clientX + offsetX;
let y = e.clientY + offsetY;
const rect = tip.getBoundingClientRect();
// Ajuster si on sort de la fenêtre
if (x + rect.width > window.innerWidth - 8) x = e.clientX - rect.width - offsetX;
if (y + rect.height > window.innerHeight - 8) y = e.clientY - rect.height - offsetY;
if (x < 4) x = 4;
if (y < 4) y = 4;
tip.style.left = x + "px";
tip.style.top = y + "px";
currentTooltipPos = { x, y };
}
// v4.2.3 : trouve l'iv correspondant au segment timeline et ouvre sa fiche
function openInterventionFromTimeline(el, opts) {
const ivIdx = el.dataset.ivIdx;
if (ivIdx === undefined) return;
const cardEl = el.closest(".card");
if (!cardEl) return;
const row = cardEl.querySelector(`.intervention-v2[data-iv-idx="${ivIdx}"]`);
if (!row) return;
const actionId = row.dataset.actionId;
if (!actionId) return;
// Récupère l'iv depuis state
const iv = findIvByActionId(actionId);
if (!iv) return;
openInterventionInNewTab(iv, opts || {});
}
function findIvByActionId(actionId) {
const data = state.currentData;
if (!data || !data.techs) return null;
for (const tech of data.techs) {
for (const iv of (tech.interventions || [])) {
if (String(iv.actionId) === String(actionId)) return iv;
}
}
return null;
}
// v4.2.3 : ouvre la GRANDE popup (comme au hover sur une ligne) mais ancrée
// juste en dessous du segment timeline cliqué, et épinglée pour qu'elle
// reste ouverte et autorise la sélection de texte.
function openPersistentTimelinePopup(el) {
const ivIdx = el.dataset.ivIdx;
if (ivIdx === undefined) return;
const cardEl = el.closest(".card");
if (!cardEl) return;
const row = cardEl.querySelector(`.intervention-v2[data-iv-idx="${ivIdx}"]`);
if (!row) return;
const actionId = row.dataset.actionId;
const iv = findIvByActionId(actionId);
if (!iv) return;
// Fermer toute popup en cours (petite timeline ou grande bulle)
const tip = tooltipEl();
if (!tip) return;
// Reset de l'état pour qu'un hideTooltip forcé ne soit pas bloqué
bulleState.pinned = false;
hideTooltip({ force: true });
// Reset position mémorisée pour éviter re-application au scroll sur
// l'ancienne position
currentTooltipPos = null;
// Construit et affiche la grande bulle, ancrée sous la timeline
tip.innerHTML = buildTooltipHTML(iv);
tip.classList.remove("hidden");
tip.classList.add("visible");
tip.dataset.mode = "anchored";
state.currentTooltipIv = iv;
// Position : juste en dessous de l'élément timeline, aligné à gauche
const r = el.getBoundingClientRect();
const tipRect = tip.getBoundingClientRect();
let x = r.left;
let y = r.bottom + 8;
if (x + tipRect.width > window.innerWidth - 8) x = window.innerWidth - tipRect.width - 8;
if (x < 4) x = 4;
if (y + tipRect.height > window.innerHeight - 8) {
// Pas assez de place en bas : on met au-dessus de la timeline
y = r.top - tipRect.height - 8;
}
if (y < 4) y = 4;
tip.style.left = x + "px";
tip.style.top = y + "px";
currentTooltipPos = { x, y };
// Épingler pour que la popup reste ouverte (elle ne se fermera que sur
// Échap, clic sur ✕ ou double-Ctrl). Permet aussi la sélection de texte.
bulleState.pinned = true;
tip.classList.add("pinned");
}
function showTimelinePopover(e, el) {
@@ -2354,10 +2587,15 @@ function showTimelinePopover(e, el) {
</dl>`;
}
const tip = tooltipEl();
// v4.2.3 : si une grande bulle est déjà épinglée via clic timeline, on ne
// la remplace pas par la petite popup hover.
if (bulleState.pinned && tip.dataset.mode === "anchored") return;
tip.innerHTML = html;
tip.classList.remove("hidden");
tip.classList.remove("hidden", "pinned");
tip.classList.add("visible");
moveTooltip(e);
// v4.2.3 : mode "hover" = petite popup qui suit la souris
tip.dataset.mode = "hover";
moveTimelineTooltip(e);
}
// ============================================================================
@@ -2785,9 +3023,15 @@ function extractContacts(raw) {
// Virer les labels parasites (Nom utilisateur, etc.) qui traînent
s = s.replace(/\b(Nom utilisateur|Utilisateur)\s*:\s*[^\n]+/gi, "");
// Séparer sur " ou ", " / ", retours à la ligne
// Mais attention : "Nom, Prénom" contient une virgule qu'on ne doit pas découper
const parts = s.split(/\s+ou\s+|\n+|\s*\/\s*(?=[A-ZÉÈÀÂÎÔÛÇ])/i)
// v4.2.3 : séparer sur plus de délimiteurs pour gérer les cas type
// "Nom1 Prénom1 +41XXXXXXXXX et Nom2 Prénom2 0XXXXXXXXX"
// Délimiteurs acceptés :
// - " ou " / " et " / " and " (mots de liaison)
// - " / " suivi d'une majuscule (nouveau contact)
// - " ; " (point-virgule)
// - saut de ligne
// IMPORTANT : on ne touche PAS aux virgules (car "Nom, Prénom" en contient).
const parts = s.split(/\s+ou\s+|\s+et\s+|\s+and\s+|\s*;\s*|\n+|\s*\/\s*(?=[A-ZÉÈÀÂÎÔÛÇ])/i)
.map(p => p.trim())
.filter(Boolean);
@@ -2829,10 +3073,16 @@ function splitOneContact(raw) {
// +41 XX XXX XX XX). On exige exactement 11 chiffres collés pour
// éviter de matcher des codes postaux ou autres nombres.
const rxLong = /(\+41\s?\d(?:[\d\s.\-]*\d)?|\+33\s?\d(?:[\d\s.\-]*\d)?|0\d(?:[\d\s.\-]*\d)?|(?<!\d)41\d{9}(?!\d)|(?<!\d)33\d{9}(?!\d))/g;
// SHORT : 5 chiffres isolés. v4.1.20 : on retire l'exigence de séparateur
// après pour tolérer "12345Texte". Avant = début, whitespace ou
// parenthèse (on évite de prendre 5 chiffres au milieu d'un long).
const rxShort = /(?:^|[\s(\/])(\d{5})(?!\d)/g;
// SHORT : numéro interne court (5 chiffres).
// - v4.1.20 : accepte "12345Texte" (pas de séparateur après)
// - v4.2.3 : accepte aussi les formats AVEC ESPACES au sein du numéro,
// typique du Canton de Vaud : "7 68 43", "6 12 34", "8 90 12".
// Doit commencer par 6, 7 ou 8 (plan de numérotation interne VD).
// Pattern : [678] + (4 autres chiffres, avec ou sans espaces/points
// intercalés, mais pas plus d'un séparateur à la fois entre 2 chiffres).
// - v4.2.3 : la version "collée" classique (5 chiffres sans espace, tout
// chiffre de début) reste acceptée comme fallback.
const rxShort = /(?:^|[\s(\/])([678](?:[\s.\-]?\d){4})(?!\d)|(?:^|[\s(\/])(\d{5})(?!\d)/g;
// Trouver toutes les positions de match pour LONG et SHORT
const matches = [];
@@ -2846,12 +3096,18 @@ function splitOneContact(raw) {
}
}
while ((mm = rxShort.exec(raw)) !== null) {
const shortTel = mm[1];
const shortStart = mm.index + mm[0].indexOf(shortTel);
const shortEnd = shortStart + shortTel.length;
const overlaps = matches.some(x => shortStart < x.end && shortEnd > x.start);
// v4.2.3 : soit le 1er groupe (format avec espaces "7 68 43"), soit le
// 2e groupe (format collé "12345") a matché.
const rawTel = mm[1] || mm[2];
if (!rawTel) continue;
// On normalise en 5 chiffres sans séparateur
const shortTel = rawTel.replace(/[\s.\-]/g, "");
if (!/^\d{5}$/.test(shortTel)) continue;
const rawStart = mm.index + mm[0].indexOf(rawTel);
const rawEnd = rawStart + rawTel.length;
const overlaps = matches.some(x => rawStart < x.end && rawEnd > x.start);
if (!overlaps) {
matches.push({ start: shortStart, end: shortEnd, tel: shortTel });
matches.push({ start: rawStart, end: rawEnd, tel: shortTel });
}
}
matches.sort((a, b) => a.start - b.start);