Rendre cliquables les adresses écrites dans l'agenda

« infos et inscriptions sur https://installparty.alpinux.org » s'affichait
en texte mort dans la fiche d'un événement : à recopier à la main. Les
adresses du descriptif et du lieu deviennent des liens, la ponctuation
qui termine la phrase restant en dehors.

Le texte vient du calendrier, donc de quelqu'un qui écrit du texte et
non du HTML : il est échappé avant d'être lié, et les titres et lieux
des cartes le sont aussi, ce qui n'était pas le cas jusqu'ici.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYfYUZRcJmmEWJwAxZb4o2
This commit is contained in:
Cédrix 2026-09-23 12:42:22 +02:00
parent 3baa10db65
commit ddf3f79e7a

View file

@ -437,6 +437,8 @@
} }
.ev-modal-body { font-size: .92rem; line-height: 1.6; color: #444; } .ev-modal-body { font-size: .92rem; line-height: 1.6; color: #444; }
.ev-modal-body a { color: #1a6bbf; word-break: break-word; }
.ev-modal-body a:hover { text-decoration: none; }
.event-date { .event-date {
font-size: .82rem; font-size: .82rem;
@ -1088,6 +1090,30 @@
} }
} }
/* Le calendrier est saisi par des humains : son texte est du texte, jamais du HTML. */
function echapper(texte) {
return String(texte == null ? '' : texte)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
/* Rend cliquables les adresses écrites dans un descriptif. Le texte reçu est déjà
échappé — le motif s'arrête donc avant une balise — et la ponctuation qui termine
une phrase reste en dehors du lien. */
function avecLiens(texteEchappe) {
return texteEchappe.replace(/https?:\/\/[^\s<]+/g, function(url) {
var queue = '';
var fin = /[.,;:!?»)\]]+$/.exec(url);
if (fin) {
queue = fin[0];
url = url.slice(0, url.length - queue.length);
}
return '<a href="' + url + '" target="_blank" rel="noopener">' + url + '</a>' + queue;
});
}
function showDetail(date, evs, el) { function showDetail(date, evs, el) {
el.hidden = false; el.hidden = false;
el.innerHTML = '<strong>' + el.innerHTML = '<strong>' +
@ -1097,9 +1123,9 @@
var time = e.allDay ? '' : var time = e.allDay ? '' :
' <span class="ev-loc">' + ' <span class="ev-loc">' +
e.start.toLocaleTimeString('fr-FR', { hour:'2-digit', minute:'2-digit' }) + '</span>'; e.start.toLocaleTimeString('fr-FR', { hour:'2-digit', minute:'2-digit' }) + '</span>';
return '<li>' + e.title + time + return '<li>' + echapper(e.title) + time +
(e.desc ? ' <span class="ev-has-desc" title="Descriptif disponible">ℹ</span>' : '') + (e.desc ? ' <span class="ev-has-desc" title="Descriptif disponible">ℹ</span>' : '') +
(e.location ? '<span class="ev-loc"> — ' + e.location + '</span>' : '') + (e.location ? '<span class="ev-loc"> — ' + echapper(e.location) + '</span>' : '') +
'</li>'; '</li>';
}).join('') + '</ul>'; }).join('') + '</ul>';
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
@ -1120,11 +1146,11 @@
var descHtml = ''; var descHtml = '';
if (ev.desc) { if (ev.desc) {
descHtml = '<p style="margin-top:.8rem;color:#555;font-size:.88rem;white-space:pre-line">' + descHtml = '<p style="margin-top:.8rem;color:#555;font-size:.88rem;white-space:pre-line">' +
ev.desc.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</p>'; avecLiens(echapper(ev.desc)) + '</p>';
} }
body.innerHTML = body.innerHTML =
'<p>📅 ' + dateStr + '</p>' + '<p>📅 ' + dateStr + '</p>' +
(ev.location ? '<p style="margin-top:.4rem">📍 ' + ev.location + '</p>' : '') + (ev.location ? '<p style="margin-top:.4rem">📍 ' + avecLiens(echapper(ev.location)) + '</p>' : '') +
descHtml; descHtml;
modal.hidden = false; modal.hidden = false;
document.getElementById('ev-modal').querySelector('.ev-modal-close').focus(); document.getElementById('ev-modal').querySelector('.ev-modal-close').focus();
@ -1156,8 +1182,8 @@
: ev.start.toLocaleTimeString('fr-FR', { hour:'2-digit', minute:'2-digit' }) + : ev.start.toLocaleTimeString('fr-FR', { hour:'2-digit', minute:'2-digit' }) +
(ev.end ? ' – ' + ev.end.toLocaleTimeString('fr-FR', { hour:'2-digit', minute:'2-digit' }) : '')) + (ev.end ? ' – ' + ev.end.toLocaleTimeString('fr-FR', { hour:'2-digit', minute:'2-digit' }) : '')) +
'</div>' + '</div>' +
'<div class="event-title">' + ev.title + '</div>' + '<div class="event-title">' + echapper(ev.title) + '</div>' +
(ev.location ? '<div class="event-where">📍 ' + ev.location + '</div>' : ''); (ev.location ? '<div class="event-where">📍 ' + echapper(ev.location) + '</div>' : '');
card.addEventListener('click', function() { openModal(ev); }); card.addEventListener('click', function() { openModal(ev); });
container.appendChild(card); container.appendChild(card);
}); });
@ -1179,8 +1205,8 @@
card.innerHTML = card.innerHTML =
(ev.desc ? '<span class="ev-has-desc" title="Descriptif disponible">ℹ</span>' : '') + (ev.desc ? '<span class="ev-has-desc" title="Descriptif disponible">ℹ</span>' : '') +
'<div class="event-date">' + fmtDate(ev.start, ev.allDay) + '</div>' + '<div class="event-date">' + fmtDate(ev.start, ev.allDay) + '</div>' +
'<div class="event-title">' + ev.title + '</div>' + '<div class="event-title">' + echapper(ev.title) + '</div>' +
(ev.location ? '<div class="event-where">📍 ' + ev.location + '</div>' : ''); (ev.location ? '<div class="event-where">📍 ' + echapper(ev.location) + '</div>' : '');
card.addEventListener('click', function() { openModal(ev); }); card.addEventListener('click', function() { openModal(ev); });
container.appendChild(card); container.appendChild(card);
}); });