PageSourceSearch

https://bioetlocal.org/wp-content/themes/bioetlocal/assets/js/evenement-filter.js?ver=1788513791

js bioetlocal.org collected 2026-10-07 21:07:04 UTC 28,824 bytes, 775 lines download raw bytes

1(function () {
2	'use strict';
3
4	var PAGE_SIZE = 10;
5
6	var PIN_SVG     = '<svg width="16" height="20" viewBox="0 0 16 20" fill="none" aria-hidden="true"><path d="M8 0C5.87827 0 3.84344 0.842855 2.34315 2.34315C0.842855 3.84344 0 5.87827 0 8C0 13.25 7 19.5 7.35 19.77C7.54762 19.9205 7.77057 20 8 20C8.22943 20 8.45238 19.9205 8.65 19.77C9 19.5 16 13.25 16 8C16 5.87827 15.1571 3.84344 13.6569 2.34315C12.1566 0.842855 10.1217 0 8 0ZM8 11C7.20887 11 6.43552 10.7654 5.77772 10.3259C5.11992 9.88633 4.60723 9.26164 4.30448 8.53073C4.00173 7.79983 3.92252 6.99556 4.07686 6.21964C4.2312 5.44372 4.61216 4.73098 5.17157 4.17157C5.73098 3.61216 6.44372 3.2312 7.21964 3.07686C7.99556 2.92252 8.79983 3.00173 9.53073 3.30448C10.2616 3.60723 10.8863 4.11992 11.3259 4.77772C11.7654 5.43552 12 6.20887 12 7C12 8.06087 11.5786 9.07828 10.8284 9.82843C10.0783 10.5786 9.06087 11 8 11Z" fill="currentColor"/></svg>';
7	var SPINNER_HTML = '<span class="evf-spinner" aria-hidden="true"></span> Localisation…';
8
9	function ready(fn) {
10		if (document.readyState !== 'loading') fn();
11		else document.addEventListener('DOMContentLoaded', fn);
12	}
13
14	ready(function () {
15		document.querySelectorAll('.ev-filter-container').forEach(initInstance);
16	});
17
18	// ── Instance ──────────────────────────────────────────────────────────────
19	function initInstance(container) {
20		if (container.dataset.filterInited) return;
21		container.dataset.filterInited = '1';
22
23		var inst = {
24			container:      container,
25			cardSel:        container.dataset.cardSelector || '.ev-filter-card',
26			offcanvas:      null,
27			pagination:     { page: 1, perPage: PAGE_SIZE },
28			userLat:        null,
29			userLng:        null,
30			leafletMap:     null,
31			leafletMarkers: [],
32			markerCluster:  null,
33			mapIcon:        container.dataset.mapIcon
34				? L.icon({
35					iconUrl:     container.dataset.mapIcon,
36					iconSize:    [32, 40],
37					iconAnchor:  [16, 40],
38					popupAnchor: [0, -42],
39				})
40				: null,
41		};
42
43		initOffcanvas(inst);
44		initFilters(inst);
45		initPagination(inst);
46		initGeo(inst);
47		initDeptCommuneFilter(inst);
48		initDateShortcuts(inst);
49		initToggleAll(inst);
50		initMapToggle(inst);
51		applyFilters(inst);
52	}
53
54	// ── Offcanvas ─────────────────────────────────────────────────────────────
55	function initOffcanvas(inst) {
56		var container   = inst.container;
57		var offcanvasId = container.id + '-offcanvas';
58		var offcanvas   = document.getElementById(offcanvasId);
59		inst.offcanvas  = offcanvas;
60
61		var backdrop = container.querySelector('.ev-offcanvas-backdrop');
62		var openBtn  = container.querySelector('.ev-btn-filtres');
63		var closeBtn = offcanvas && offcanvas.querySelector('.ev-offcanvas-close');
64		var applyBtn = offcanvas && offcanvas.querySelector('.ev-btn-apply-filters');
65
66		function openPanel() {
67			if (!offcanvas) return;
68			offcanvas.classList.add('open');
69			if (backdrop) backdrop.classList.add('open');
70			if (openBtn) openBtn.setAttribute('aria-expanded', 'true');
71			document.body.style.overflow = 'hidden';
72		}
73
74		function closePanel() {
75			if (!offcanvas) return;
76			offcanvas.classList.remove('open');
77			if (backdrop) backdrop.classList.remove('open');
78			if (openBtn) openBtn.setAttribute('aria-expanded', 'false');
79			document.body.style.overflow = '';
80		}
81
82		inst.closePanel = closePanel;
83
84		if (openBtn)  openBtn.addEventListener('click',  openPanel);
85		if (closeBtn) closeBtn.addEventListener('click', closePanel);
86		if (backdrop) backdrop.addEventListener('click', closePanel);
87
88		if (applyBtn) {
89			applyBtn.addEventListener('click', function () {
90				inst.pagination.page = 1;
91				applyFilters(inst);
92				closePanel();
93			});
94		}
95
96		document.addEventListener('keydown', function (e) {
97			if (e.key === 'Escape' && offcanvas && offcanvas.classList.contains('open')) {
98				closePanel();
99			}
100		});
101	}
102
103	// ── Géolocalisation ───────────────────────────────────────────────────────
104	function initGeo(inst) {
105		var btns = inst.container.querySelectorAll('.evf-autour-de-moi');
106		if (!btns.length) return;
107
108		function setLoading() {
109			btns.forEach(function (b) { b.innerHTML = SPINNER_HTML; b.disabled = true; });
110		}
111
112		function setActive() {
113			btns.forEach(function (b) {
114				b.classList.add('evf-autour-de-moi--active');
115				b.innerHTML = PIN_SVG + ' Position acquise ✓';
116				b.disabled = false;
117			});
118		}
119
120		function setReset() {
121			btns.forEach(function (b) {
122				b.classList.remove('evf-autour-de-moi--active');
123				b.textContent = 'Afficher les événements autour de moi';
124				b.disabled = false;
125			});
126		}
127
128		btns.forEach(function (btn) {
129			btn.addEventListener('click', function () {
130				if (inst.userLat !== null) {
131					inst.userLat = null;
132					inst.userLng = null;
133					var rayonSel = inst.offcanvas && inst.offcanvas.querySelector('.evf-select-rayon');
134					if (rayonSel && rayonSel.value === '20') rayonSel.value = '';
135					setReset();
136					inst.pagination.page = 1;
137					applyFilters(inst);
138					return;
139				}
140				if (!navigator.geolocation) {
141					alert("La géolocalisation n'est pas disponible sur ce navigateur.");
142					return;
143				}
144				setLoading();
145				navigator.geolocation.getCurrentPosition(
146					function (pos) {
147						inst.userLat = pos.coords.latitude;
148						inst.userLng = pos.coords.longitude;
149						setActive();
150						if (inst.closePanel) inst.closePanel();
151						// Rayon par défaut si aucun sélectionné dans l'offcanvas
152						var rayonSel = inst.offcanvas && inst.offcanvas.querySelector('.evf-select-rayon');
153						if (rayonSel && !rayonSel.value) rayonSel.value = '20';
154						inst.pagination.page = 1;
155						applyFilters(inst);
156						scrollToList(inst);
157					},
158					function () {
159						setReset();
160						alert("Impossible d'obtenir votre position.");
161					}
162				);
163			});
164		});
165	}
166
167	// ── Restreindre communes par département ──────────────────────────────────
168	function initDeptCommuneFilter(inst) {
169		var offcanvas = inst.offcanvas;
170		if (!offcanvas) return;
171
172		var deptSel = offcanvas.querySelector('.evf-select-dept');
173		var communeSel = offcanvas.querySelector('.evf-select-commune');
174		if (!deptSel || !communeSel) return;
175
176		deptSel.addEventListener('change', function () {
177			var selectedDept = deptSel.value;
178			var communeOptions = communeSel.querySelectorAll('option');
179
180			communeOptions.forEach(function (opt) {
181				if (!opt.value) {
182					// Option "Toutes les communes" : toujours visible
183					opt.hidden = false;
184					opt.disabled = false;
185					return;
186				}
187				var optDepts = opt.dataset.dept ? opt.dataset.dept.split(' ') : [];
188				if (selectedDept && !optDepts.includes(selectedDept)) {
189					opt.hidden = true;
190					opt.disabled = true;
191				} else {
192					opt.hidden = false;
193					opt.disabled = false;
194				}
195			});
196
197			// Si la commune actuellement sélectionnée est devenue invisible, réinitialiser
198			if (communeSel.value && communeSel.options[communeSel.selectedIndex].hidden) {
199				communeSel.value = '';
200			}
201		});
202	}
203
204	// ── Raccourcis date ───────────────────────────────────────────────────────
205	function initDateShortcuts(inst) {
206		var offcanvas = inst.offcanvas;
207		if (!offcanvas) return;
208
209		offcanvas.querySelectorAll('.evf-shortcut-btn').forEach(function (btn) {
210			btn.addEventListener('click', function () {
211				var isActive = btn.classList.contains('evf-shortcut-btn--active');
212
213				offcanvas.querySelectorAll('.evf-shortcut-btn').forEach(function (b) {
214					b.classList.remove('evf-shortcut-btn--active');
215				});
216
217				if (!isActive) {
218					btn.classList.add('evf-shortcut-btn--active');
219				}
220
221				var jourSel  = offcanvas.querySelector('.evf-select-jour');
222				var moisSel  = offcanvas.querySelector('.evf-select-mois');
223				if (jourSel) jourSel.value = '';
224				if (moisSel) moisSel.value = '';
225			});
226		});
227
228		['.evf-select-jour', '.evf-select-mois'].forEach(function (sel) {
229			var el = offcanvas.querySelector(sel);
230			if (el) {
231				el.addEventListener('change', function () {
232					offcanvas.querySelectorAll('.evf-shortcut-btn').forEach(function (b) {
233						b.classList.remove('evf-shortcut-btn--active');
234					});
235				});
236			}
237		});
238	}
239
240	// ── Tout cocher / décocher ────────────────────────────────────────────────
241	function initToggleAll(inst) {
242		var offcanvas = inst.offcanvas;
243		if (!offcanvas) return;
244
245		offcanvas.querySelectorAll('.evf-toggle-all').forEach(function (btn) {
246			btn.addEventListener('click', function () {
247				var group     = btn.dataset.group;
248				var checkboxes = offcanvas.querySelectorAll('[data-group="' + group + '"]');
249				var allChecked = Array.prototype.every.call(checkboxes, function (cb) { return cb.checked; });
250
251				checkboxes.forEach(function (cb) {
252					cb.checked = !allChecked;
253				});
254			});
255		});
256	}
257
258	// ── Filtres ───────────────────────────────────────────────────────────────
259	function initFilters(inst) {
260		var container  = inst.container;
261		var offcanvas  = inst.offcanvas;
262		var searchEl   = container.querySelector('.ev-search-text');
263
264		if (searchEl) {
265			var debounceTimer;
266			searchEl.addEventListener('input', function () {
267				inst.pagination.page = 1;
268				clearTimeout(debounceTimer);
269				debounceTimer = setTimeout(function () { applyFilters(inst); }, 220);
270			});
271		}
272
273		var resetBtn = offcanvas && offcanvas.querySelector('.ev-btn-reset-filters');
274		if (resetBtn) {
275			resetBtn.addEventListener('click', function () {
276				if (searchEl) searchEl.value = '';
277
278				if (offcanvas) {
279					offcanvas.querySelectorAll('input[type="checkbox"]').forEach(function (cb) { cb.checked = false; });
280					offcanvas.querySelectorAll('select').forEach(function (sel) { sel.value = ''; });
281					offcanvas.querySelectorAll('.evf-shortcut-btn').forEach(function (b) {
282						b.classList.remove('evf-shortcut-btn--active');
283					});
284					// Réafficher toutes les communes quand on réinitialise
285					var communeOptions = offcanvas.querySelectorAll('.evf-select-commune option');
286					communeOptions.forEach(function (opt) {
287						opt.hidden = false;
288						opt.disabled = false;
289					});
290				}
291
292				container.querySelectorAll('.evf-autour-de-moi').forEach(function (btn) {
293					btn.classList.remove('evf-autour-de-moi--active');
294					btn.textContent = 'Afficher les événements autour de moi';
295				});
296				inst.userLat = null;
297				inst.userLng = null;
298
299				inst.pagination.page = 1;
300				applyFilters(inst);
301			});
302		}
303	}
304
305	// ── Pagination (prev / next) ──────────────────────────────────────────────
306	function initPagination(inst) {
307		var prevBtn = inst.container.querySelector('.ev-pag-prev');
308		var nextBtn = inst.container.querySelector('.ev-pag-next');
309
310		if (!prevBtn && !nextBtn) { inst.pagination.perPage = Infinity; return; }
311
312		if (prevBtn) {
313			prevBtn.addEventListener('click', function () {
314				if (inst.pagination.page > 1) {
315					inst.pagination.page--;
316					applyFilters(inst);
317					scrollToList(inst);
318				}
319			});
320		}
321		if (nextBtn) {
322			nextBtn.addEventListener('click', function () {
323				inst.pagination.page++;
324				applyFilters(inst);
325				scrollToList(inst);
326			});
327		}
328
329		// Redessine la pagination (delta plus ou moins large selon la largeur,
330		// voir renderPagination) quand l'écran est redimensionné, pas seulement
331		// au chargement de la page.
332		var resizeTimer;
333		window.addEventListener('resize', function () {
334			clearTimeout(resizeTimer);
335			resizeTimer = setTimeout(function () {
336				if (inst.pagination.totalPages) renderPagination(inst, inst.pagination.totalPages);
337			}, 150);
338		});
339	}
340
341	function scrollToList(inst) {
342		var list = inst.container.querySelector('.ev-arc-events-list');
343		if (!list) return;
344		var header = document.querySelector('#site-header');
345		var offset = (header ? header.offsetHeight : 0) + 16;
346		var top = list.getBoundingClientRect().top + window.pageYOffset - offset;
347		window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
348	}
349
350	// ── Rendu de la pagination numérotée ─────────────────────────────────────
351	function renderPagination(inst, totalPages) {
352		var container  = inst.container;
353		var paginationEl = container.querySelector('.ev-pagination');
354		var pagesEl    = container.querySelector('.ev-pag-pages');
355		var prevBtn    = container.querySelector('.ev-pag-prev');
356		var nextBtn    = container.querySelector('.ev-pag-next');
357
358		if (!paginationEl || !pagesEl) return;
359
360		inst.pagination.totalPages = totalPages;
361
362		// Masquée aussi tant que la carte est affichée (voir initMapToggle) : la
363		// carte montre tous les événements filtrés d'un coup, la pagination n'a
364		// plus de sens dans ce mode.
365		paginationEl.hidden = inst.mapVisible || totalPages <= 1;
366		if (totalPages <= 1) return;
367
368		if (prevBtn) prevBtn.disabled = inst.pagination.page <= 1;
369		if (nextBtn) nextBtn.disabled = inst.pagination.page >= totalPages;
370
371		// Tronque la liste de pages (1 … page-delta..page+delta … dernière) au lieu
372		// de tout afficher : avec beaucoup de pages, ça débordait de l'écran sur
373		// petit écran. Fenêtre (delta) plus large à mesure que l'écran grandit.
374		var current = inst.pagination.page;
375		var delta   = window.innerWidth < 576 ? 0 : window.innerWidth < 1200 ? 2 : 4;
376		var pages   = [];
377		for (var i = 1; i <= totalPages; i++) {
378			if (i === 1 || i === totalPages || (i >= current - delta && i <= current + delta)) {
379				pages.push(i);
380			} else if (pages[pages.length - 1] !== '…') {
381				pages.push('…');
382			}
383		}
384
385		pagesEl.innerHTML = '';
386		pages.forEach(function (p) {
387			if (p === '…') {
388				var span = document.createElement('span');
389				span.className = 'ev-pag-ellipsis';
390				span.textContent = '…';
391				span.setAttribute('aria-hidden', 'true');
392				pagesEl.appendChild(span);
393				return;
394			}
395			var btn = document.createElement('button');
396			btn.type = 'button';
397			btn.className = 'ev-pag-btn' + (p === current ? ' ev-pag-btn--active' : '');
398			btn.textContent = p;
399			btn.setAttribute('aria-label', 'Page ' + p);
400			if (p === current) btn.setAttribute('aria-current', 'page');
401			btn.addEventListener('click', function () {
402				inst.pagination.page = p;
403				applyFilters(inst);
404				scrollToList(inst);
405			});
406			pagesEl.appendChild(btn);
407		});
408	}
409
410	// ── Application des filtres ───────────────────────────────────────────────
411	function applyFilters(inst) {
412		var container = inst.container;
413		var offcanvas = inst.offcanvas;
414		var searchEl  = container.querySelector('.ev-search-text');
415
416		var needle  = searchEl ? searchEl.value.trim().toLowerCase() : '';
417
418		var checkedCats = offcanvas
419			? Array.prototype.map.call(
420				offcanvas.querySelectorAll('.evf-cat-activite:checked'),
421				function (cb) { return cb.value; }
422			) : [];
423
424		var checkedProduits = offcanvas
425			? Array.prototype.map.call(
426				offcanvas.querySelectorAll('.evf-filter-produit:checked'),
427				function (cb) { return cb.value; }
428			) : [];
429
430		var checkedModalites = offcanvas
431			? Array.prototype.map.call(
432				offcanvas.querySelectorAll('.evf-filter-modalite:checked'),
433				function (cb) { return cb.value; }
434			) : [];
435
436		var dept      = offcanvas ? (offcanvas.querySelector('.evf-select-dept')    || {value: ''}).value : '';
437		var communeSelEl = offcanvas ? offcanvas.querySelector('.evf-select-commune') : null;
438		var commune   = communeSelEl ? communeSelEl.value : '';
439		var rayon     = offcanvas ? parseFloat((offcanvas.querySelector('.evf-select-rayon') || {value: ''}).value) || 0 : 0;
440
441		// Origine du rayon : priorité à la géolocalisation ("autour de moi"),
442		// sinon les coordonnées de la commune sélectionnée (portées en
443		// data-lat/data-lng sur son <option>, cf. evenement-filter.php).
444		var originLat = inst.userLat;
445		var originLng = inst.userLng;
446		if (originLat === null && commune && communeSelEl) {
447			var communeOpt = communeSelEl.options[communeSelEl.selectedIndex];
448			var optLat = communeOpt ? parseFloat(communeOpt.dataset.lat) : NaN;
449			var optLng = communeOpt ? parseFloat(communeOpt.dataset.lng) : NaN;
450			if (!isNaN(optLat) && !isNaN(optLng)) {
451				originLat = optLat;
452				originLng = optLng;
453			}
454		}
455
456		var activeShortcut = '';
457		if (offcanvas) {
458			var activeShortcutBtn = offcanvas.querySelector('.evf-shortcut-btn--active');
459			if (activeShortcutBtn) activeShortcut = activeShortcutBtn.dataset.shortcut || '';
460		}
461
462		var jourFilter = offcanvas ? parseInt((offcanvas.querySelector('.evf-select-jour') || {value: ''}).value, 10) : NaN;
463		var moisFilter = offcanvas ? parseInt((offcanvas.querySelector('.evf-select-mois') || {value: ''}).value, 10) : NaN;
464
465		// 1. Collecter les cartes qui passent les filtres
466		var matchedCards = [];
467
468		container.querySelectorAll(inst.cardSel).forEach(function (card) {
469			var ok = true;
470
471			if (ok && needle) {
472				ok = (card.dataset.search || '').indexOf(needle) !== -1;
473			}
474
475			if (ok && checkedCats.length > 0) {
476				var cardCats = (card.dataset.cats || '').split(' ').filter(Boolean);
477				ok = checkedCats.some(function (c) { return cardCats.indexOf(c) !== -1; });
478			}
479
480			if (ok && checkedProduits.length > 0) {
481				var cardProduits = (card.dataset.produits || '').split(' ').filter(Boolean);
482				ok = checkedProduits.some(function (p) { return cardProduits.indexOf(p) !== -1; });
483			}
484
485			if (ok && checkedModalites.length > 0) {
486				var cardModalites = (card.dataset.modalites || '').split(' ').filter(Boolean);
487				ok = checkedModalites.some(function (m) { return cardModalites.indexOf(m) !== -1; });
488			}
489
490			if (ok && dept) {
491				ok = (card.dataset.dept || '') === dept;
492			}
493
494			// Filtre commune exacte : ignoré si un rayon est actif, sinon la
495			// commune restreindrait déjà aux seuls événements de cette commune
496			// et le rayon (censé chercher autour) n'aurait plus aucun effet.
497			if (ok && commune && !(rayon > 0)) {
498				ok = (card.dataset.commune || '') === commune;
499			}
500
501			if (ok && rayon > 0 && originLat !== null && originLng !== null) {
502				var lat = parseFloat(card.dataset.lat);
503				var lng = parseFloat(card.dataset.lng);
504				if (!isNaN(lat) && !isNaN(lng)) {
505					ok = haversine(originLat, originLng, lat, lng) <= rayon;
506				} else {
507					ok = false;
508				}
509			}
510
511			if (ok && activeShortcut) {
512				var ymd = card.dataset.dateDebut || '';
513				if (activeShortcut === 'weekend') {
514					var wr = getWeekendRange();
515					ok = ymd >= wr[0] && ymd <= wr[1];
516				} else if (activeShortcut === 'week') {
517					var wkr = getWeekRange();
518					ok = ymd >= wkr[0] && ymd <= wkr[1];
519				}
520			}
521
522			if (ok && !isNaN(jourFilter) && !activeShortcut) {
523				var ymdJ = card.dataset.dateDebut || '';
524				if (ymdJ) {
525					var dJ = new Date(ymdJ.substring(0,4) + '-' + ymdJ.substring(4,6) + '-' + ymdJ.substring(6,8));
526					ok = dJ.getDate() === jourFilter;
527				} else {
528					ok = false;
529				}
530			}
531
532			if (ok && !isNaN(moisFilter) && !activeShortcut) {
533				var ymdM = card.dataset.dateDebut || '';
534				if (ymdM) {
535					var dM = new Date(ymdM.substring(0,4) + '-' + ymdM.substring(4,6) + '-' + ymdM.substring(6,8));
536					ok = (dM.getMonth() + 1) === moisFilter;
537				} else {
538					ok = false;
539				}
540			}
541
542			card.hidden = true;
543			if (card.parentElement) {
544				card.parentElement.hidden = true;
545				card.parentElement.classList.remove('col-lg-12');
546			}
547			// Indépendant de la pagination (voir 2. plus bas) : sert de source de
548			// vérité pour la carte, qui doit montrer tous les événements filtrés,
549			// pas seulement ceux de la page courante.
550			card.dataset.filterMatch = ok ? '1' : '0';
551			if (ok) matchedCards.push(card);
552		});
553
554		// 2. Paginer : afficher uniquement la tranche de la page courante
555		var perPage    = inst.pagination.perPage;
556		var totalPages = Math.max(1, Math.ceil(matchedCards.length / perPage));
557
558		if (inst.pagination.page > totalPages) inst.pagination.page = totalPages;
559
560		var startIdx = (inst.pagination.page - 1) * perPage;
561		matchedCards.forEach(function (card, i) {
562			card.hidden = i < startIdx || i >= startIdx + perPage;
563			if (card.parentElement) card.parentElement.hidden = card.hidden;
564		});
565
566		// Dernière card seule sur sa rangée → pleine largeur
567		var pageCount = Math.min(perPage, matchedCards.length - startIdx);
568		if (pageCount % 2 !== 0) {
569			var lastCard = matchedCards[startIdx + pageCount - 1];
570			if (lastCard && lastCard.parentElement) lastCard.parentElement.classList.add('col-lg-12');
571		}
572
573		// 3. Compteur
574		var countEl = container.querySelector('.ev-results-count');
575		if (countEl) {
576			var count = matchedCards.length;
577			countEl.textContent = count + ' événement' + (count > 1 ? 's' : '');
578		}
579
580		// 4. Rendu de la pagination
581		renderPagination(inst, totalPages);
582
583		// 5. Sync marqueurs carte si elle est ouverte
584		syncMapMarkers(inst);
585	}
586
587	// ── Carte interactive (toggle liste ↔ carte) ──────────────────────────────
588	function initMapToggle(inst) {
589		var btn          = inst.container.querySelector('.ev-btn-carte');
590		var mapContainer = inst.container.querySelector('.ev-arc-map-container');
591
592		if (!btn || !mapContainer) return;
593
594		inst.mapBtn          = btn;
595		inst.mapContainerEl  = mapContainer;
596
597		btn.addEventListener('click', function () {
598			setMapVisible(inst, !inst.mapVisible);
599		});
600
601		// Carte affichée par défaut au chargement.
602		setMapVisible(inst, true);
603	}
604
605	// ── Bascule effective liste ↔ carte (appelée au clic et à l'init) ─────────
606	function setMapVisible(inst, mapVisible) {
607		var btn          = inst.mapBtn;
608		var mapContainer = inst.mapContainerEl;
609		var listEl       = inst.container.querySelector('.ev-arc-events-list');
610		var paginationEl = inst.container.querySelector('.ev-pagination');
611
612		inst.mapVisible = mapVisible;
613
614		mapContainer.hidden = !mapVisible;
615		if (listEl) listEl.hidden = mapVisible;
616		// La pagination n'a pas de sens en mode carte (qui montre tous les
617		// événements filtrés d'un coup, voir syncMapMarkers) : masquée tant
618		// que la carte est affichée, restaurée selon le nombre de pages au
619		// retour en liste. renderPagination() respecte aussi inst.mapVisible
620		// pour ne pas la réafficher si les filtres changent carte ouverte.
621		if (paginationEl) {
622			paginationEl.hidden = mapVisible || inst.pagination.totalPages <= 1;
623		}
624
625		btn.textContent = mapVisible ? 'Afficher la liste' : 'Afficher la carte';
626
627		if (!mapVisible) return;
628
629		if (inst.leafletMap) {
630			inst.leafletMap.invalidateSize();
631			syncMapMarkers(inst);
632			return;
633		}
634
635		buildMap(inst, mapContainer);
636	}
637
638	// ── Construction initiale de la carte Leaflet (une seule fois) ────────────
639	function buildMap(inst, mapContainer) {
640		if (typeof L === 'undefined' || typeof BioetlocalMap === 'undefined') return;
641
642		var mapEl = document.createElement('div');
643		mapEl.className = 'ev-arc-map-inner';
644		mapContainer.appendChild(mapEl);
645
646		var seeds = [];
647		inst.container.querySelectorAll(inst.cardSel).forEach(function (card) {
648			var lat = parseFloat(card.dataset.lat);
649			var lng = parseFloat(card.dataset.lng);
650			if (!isNaN(lat) && !isNaN(lng) && lat !== 0 && lng !== 0) {
651				seeds.push({
652					lat:   lat,
653					lng:   lng,
654					title: card.getAttribute('aria-label') || '',
655					url:   card.getAttribute('href') || '#',
656					card:  card,
657				});
658			}
659		});
660
661		if (!seeds.length) {
662			mapContainer.innerHTML = '<p class="ev-arc-empty" style="padding:1rem">Aucun événement géolocalisé.</p>';
663			return;
664		}
665
666		var lats = seeds.map(function (m) { return m.lat; });
667		var lngs = seeds.map(function (m) { return m.lng; });
668		var centerLat = (Math.min.apply(null, lats) + Math.max.apply(null, lats)) / 2;
669		var centerLng = (Math.min.apply(null, lngs) + Math.max.apply(null, lngs)) / 2;
670
671		// Sur petit écran, désactiver le pan tactile au départ (le pincement
672		// à 2 doigts pour zoomer reste actif) et l'activer au premier tap :
673		// sinon le scroll de la page se fait happer par le glisser-carte
674		// dès que le doigt passe dessus (peu de marge autour sur mobile).
675		var smallScreen = window.matchMedia('(max-width: 767px)').matches;
676		inst.leafletMap = L.map(mapEl, { dragging: !smallScreen }
676).setView([centerLat, centerLng], 9);
677		BioetlocalMap.addOsmTiles(inst.leafletMap);
678		if (smallScreen) {
679			inst.leafletMap.once('click', function () { inst.leafletMap.dragging.enable(); });
680		}
681
682		inst.markerCluster = typeof L.markerClusterGroup === 'function'
683			? L.markerClusterGroup()
684			: L.layerGroup(); // dégradation silencieuse si le plugin ne charge pas
685
686		seeds.forEach(function (m) {
687			var popupHtml = '<a href="' + m.url + '" class="ev-arc-card ev-popup-card">' + m.card.innerHTML + '</a>';
688			var markerOpts = inst.mapIcon ? { icon: inst.mapIcon } : {};
689			var lm = L.marker([m.lat, m.lng], markerOpts)
690				.bindPopup(popupHtml, { maxWidth: 280, minWidth: 260 });
691			inst.markerCluster.addLayer(lm);
692			inst.leafletMarkers.push({ marker: lm, card: m.card });
693		});
694
695		inst.leafletMap.addLayer(inst.markerCluster);
696
697		if (seeds.length > 1) {
698			var bounds = L.latLngBounds(seeds.map(function (m) { return [m.lat, m.lng]; }));
699			inst.leafletMap.fitBounds(bounds, { padding: [40, 40] });
700		}
701
702		syncMapMarkers(inst);
703	}
704
705	// ── Sync marqueurs carte avec état filtré ──────────────────────────────────
706	function syncMapMarkers(inst) {
707		if (!inst.leafletMarkers.length || !inst.leafletMap) return;
708		var mapContainer = inst.container.querySelector('.ev-arc-map-container');
709		if (!mapContainer || mapContainer.hidden) return;
710
711		// Basé sur dataset.filterMatch (indépendant de la pagination) : montre
712		// tous les événements qui correspondent aux filtres actifs d'un coup,
713		// pas seulement ceux de la page de liste actuellement affichée.
714		// Ajout/retrait réel du groupe de clustering (pas juste une opacité) :
715		// un marqueur simplement rendu transparent resterait compté dans le
716		// chiffre du cluster, ce qui fausserait le regroupement affiché.
717		var visibleLatLngs = [];
718		inst.leafletMarkers.forEach(function (lm) {
719			var show     = lm.card.dataset.filterMatch === '1';
720			var inLayer  = inst.markerCluster && inst.markerCluster.hasLayer(lm.marker);
721			if (inst.markerCluster) {
722				if (show && !inLayer) inst.markerCluster.addLayer(lm.marker);
723				else if (!show && inLayer) inst.markerCluster.removeLayer(lm.marker);
724			}
725			if (show) visibleLatLngs.push([lm.marker.getLatLng().lat, lm.marker.getLatLng().lng]);
726		});
727
728		if (visibleLatLngs.length > 1) {
729			inst.leafletMap.fitBounds(L.latLngBounds(visibleLatLngs), { padding: [40, 40] });
730		} else if (visibleLatLngs.length === 1) {
731			inst.leafletMap.setView(visibleLatLngs[0], 13);
732		}
733	}
734
735	// ── Helpers ───────────────────────────────────────────────────────────────
736	function haversine(lat1, lng1, lat2, lng2) {
737		var R = 6371;
738		var dLat = (lat2 - lat1) * Math.PI / 180;
739		var dLng = (lng2 - lng1) * Math.PI / 180;
740		var a = Math.sin(dLat/2) * Math.sin(dLat/2)
741			+ Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180)
742			* Math.sin(dLng/2) * Math.sin(dLng/2);
743		return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
744	}
745
746	function ymdFromDate(d) {
747		var y   = d.getFullYear();
748		var m   = String(d.getMonth() + 1).padStart(2, '0');
749		var day = String(d.getDate()).padStart(2, '0');
750		return '' + y + m + day;
751	}
752
753	function getWeekendRange() {
754		var now = new Date();
755		var day = now.getDay();
756		var diffToSat = (6 - day + 7) % 7 || 7;
757		var sat = new Date(now);
758		sat.setDate(now.getDate() + diffToSat);
759		var sun = new Date(sat);
760		sun.setDate(sat.getDate() + 1);
761		return [ymdFromDate(sat), ymdFromDate(sun)];
762	}
763
764	function getWeekRange() {
765		var now = new Date();
766		var day = now.getDay();
767		var diffToMon = (day === 0 ? -6 : 1 - day);
768		var mon = new Date(now);
769		mon.setDate(now.getDate() + diffToMon);
770		var sun = new Date(mon);
771		sun.setDate(mon.getDate() + 6);
772		return [ymdFromDate(mon), ymdFromDate(sun)];
773	}
774
775})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.