Files

275 lines
8.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);