Publication initiale de RelaisSMS : documentation et sources nettoyees
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user