const debounce = (callback, delay = 180) => { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => callback(...args), delay); }; }; const createRecipientChip = (candidate) => { const chip = document.createElement('span'); chip.className = [ 'recipient-chip', candidate.type === 'group' ? 'group' : '', candidate.is_favorite ? 'favorite' : '', ].filter(Boolean).join(' '); chip.dataset.recipientToken = ''; chip.dataset.type = candidate.type; chip.dataset.id = String(candidate.id); const label = document.createElement('span'); label.textContent = candidate.label; const remove = document.createElement('button'); remove.type = 'button'; remove.dataset.recipientRemove = ''; remove.setAttribute('aria-label', `Retirer ${candidate.label}`); remove.textContent = '×'; const hidden = document.createElement('input'); hidden.type = 'hidden'; hidden.name = candidate.type === 'group' ? 'group_ids[]' : 'contact_ids[]'; hidden.value = String(candidate.id); chip.append(label, remove, hidden); return chip; }; const initRecipientComposer = (composer) => { const searchUrl = composer.dataset.searchUrl; const selected = composer.querySelector('[data-recipient-selected]'); const input = composer.querySelector('[data-recipient-input]'); const results = composer.querySelector('[data-recipient-results]'); if (!searchUrl || !selected || !input || !results) { return; } const selectedKeys = new Set( [...selected.querySelectorAll('[data-recipient-token]')].map((chip) => `${chip.dataset.type}:${chip.dataset.id}`), ); let candidates = []; let activeIndex = -1; const hideResults = () => { results.hidden = true; results.replaceChildren(); activeIndex = -1; }; const addCandidate = (candidate) => { const key = `${candidate.type}:${candidate.id}`; if (selectedKeys.has(key)) { input.value = ''; hideResults(); return; } selectedKeys.add(key); selected.insertBefore(createRecipientChip(candidate), input); input.value = ''; hideResults(); input.focus(); }; const renderResults = () => { results.replaceChildren(); if (candidates.length === 0) { const empty = document.createElement('div'); empty.className = 'recipient-result'; empty.textContent = 'Aucun resultat'; results.append(empty); results.hidden = false; return; } candidates.forEach((candidate, index) => { const button = document.createElement('button'); button.type = 'button'; button.className = `recipient-result${index === activeIndex ? ' active' : ''}`; button.dataset.index = String(index); const main = document.createElement('span'); const title = document.createElement('strong'); title.textContent = `${candidate.is_favorite ? '★ ' : ''}${candidate.label}`; const meta = document.createElement('small'); meta.textContent = candidate.meta || candidate.source || ''; main.append(title, document.createElement('br'), meta); const type = document.createElement('small'); type.textContent = candidate.type === 'group' ? 'Groupe' : 'Contact'; button.append(main, type); button.addEventListener('mousedown', (event) => { event.preventDefault(); addCandidate(candidate); }); results.append(button); }); results.hidden = false; }; const fitResultsToViewport = () => { const rect = results.getBoundingClientRect(); const availableHeight = window.innerHeight - rect.top - 16; results.style.maxHeight = `${Math.max(180, availableHeight)}px`; }; const search = debounce(async () => { const query = input.value.trim(); const url = new URL(searchUrl, window.location.origin); url.searchParams.set('q', query); const response = await fetch(url, { headers: { Accept: 'application/json', }, }); if (!response.ok) { hideResults(); return; } const payload = await response.json(); candidates = (payload.results || []).filter( (candidate) => !selectedKeys.has(`${candidate.type}:${candidate.id}`), ); activeIndex = candidates.length > 0 ? 0 : -1; renderResults(); fitResultsToViewport(); }); input.addEventListener('input', search); input.addEventListener('focus', search); window.addEventListener('resize', fitResultsToViewport); input.addEventListener('keydown', (event) => { if (event.key === 'Escape') { hideResults(); return; } if (event.key === 'Backspace' && input.value === '') { const chip = input.previousElementSibling; if (chip?.matches('[data-recipient-token]')) { selectedKeys.delete(`${chip.dataset.type}:${chip.dataset.id}`); chip.remove(); } return; } if (results.hidden || candidates.length === 0) { return; } if (event.key === 'ArrowDown') { event.preventDefault(); activeIndex = (activeIndex + 1) % candidates.length; renderResults(); } if (event.key === 'ArrowUp') { event.preventDefault(); activeIndex = (activeIndex - 1 + candidates.length) % candidates.length; renderResults(); } if (event.key === 'Enter' && activeIndex >= 0) { event.preventDefault(); addCandidate(candidates[activeIndex]); } }); selected.addEventListener('click', (event) => { const remove = event.target.closest('[data-recipient-remove]'); if (!remove) { return; } const chip = remove.closest('[data-recipient-token]'); selectedKeys.delete(`${chip.dataset.type}:${chip.dataset.id}`); chip.remove(); input.focus(); }); document.addEventListener('click', (event) => { if (!composer.contains(event.target)) { hideResults(); } }); }; document.querySelectorAll('[data-recipient-composer]').forEach(initRecipientComposer); const initScheduleControls = (toggle) => { const form = toggle.closest('form'); const panel = form?.querySelector('[data-schedule-panel]'); const input = form?.querySelector('[data-schedule-input]'); const submit = form?.querySelector('[data-schedule-submit]'); if (!form || !panel || !input || !submit) { return; } const showPanel = () => { panel.hidden = false; submit.hidden = false; input.required = true; toggle.setAttribute('aria-expanded', 'true'); input.focus(); }; if (!panel.hidden) { input.required = true; } toggle.addEventListener('click', showPanel); }; document.querySelectorAll('[data-schedule-toggle]').forEach(initScheduleControls); const initMessageComposer = (composer) => { const form = composer.closest('form'); const body = form?.querySelector('[data-message-body]'); const includeHeader = composer.querySelector('[data-message-include-header]'); const includeFooter = composer.querySelector('[data-message-include-footer]'); const preview = composer.querySelector('[data-message-preview]'); const header = composer.dataset.header || ''; const footer = composer.dataset.footer || ''; if (!form || !body || !preview) { return; } const render = () => { const parts = []; if (includeHeader?.checked && header.trim() !== '') { parts.push(header.trim()); } if (body.value.trim() !== '') { parts.push(body.value.trim()); } if (includeFooter?.checked && footer.trim() !== '') { parts.push(footer.trim()); } preview.textContent = parts.join('\n'); }; body.addEventListener('input', render); includeHeader?.addEventListener('change', render); includeFooter?.addEventListener('change', render); render(); }; document.querySelectorAll('[data-message-composer]').forEach(initMessageComposer);