Publication initiale de RelaisSMS : documentation et sources nettoyees

This commit is contained in:
2026-10-21 09:29:39 +02:00
commit ac186b03b1
158 changed files with 24818 additions and 0 deletions
+274
View File
@@ -0,0 +1,274 @@
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);