275 lines
8.4 KiB
JavaScript
275 lines
8.4 KiB
JavaScript
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);
|