Tag progetto, @menzioni, appuntamenti da conversazioni
- Modello Tag con nome e colore, M2M su Conversazione - Modello Appuntamento con luogo, note, partecipanti, link a Conversazione - @menzioni nei commenti e aggiornamenti: @username → link al profilo - Autocomplete JS per @menzioni nelle textarea - Auto-data conversazioni (default=now) - CRUD completo appuntamenti con permessi autore - Appuntamenti in agenda, dashboard, dettaglio conversazione - Crea riunione direttamente da una conversazione (pre-compila titolo e partecipanti) - Admin: Tag, Appuntamento registrati
This commit is contained in:
@@ -148,6 +148,102 @@ function initApp() {
|
||||
convertMessagesToToasts();
|
||||
initSliders();
|
||||
initLiveSearch();
|
||||
initMentions();
|
||||
}
|
||||
|
||||
// ── @Mentions autocomplete ──
|
||||
function initMentions() {
|
||||
document.querySelectorAll('textarea.form-control').forEach(textarea => {
|
||||
if (textarea._mentionInit) return;
|
||||
textarea._mentionInit = true;
|
||||
|
||||
let dropdown = textarea.parentElement.querySelector('.mention-dropdown');
|
||||
if (!dropdown) {
|
||||
dropdown = document.createElement('div');
|
||||
dropdown.className = 'mention-dropdown';
|
||||
textarea.parentElement.style.position = 'relative';
|
||||
textarea.parentElement.appendChild(dropdown);
|
||||
}
|
||||
|
||||
let mentionStart = -1;
|
||||
let timer = null;
|
||||
|
||||
textarea.addEventListener('input', () => {
|
||||
const pos = textarea.selectionStart;
|
||||
const text = textarea.value.substring(0, pos);
|
||||
const atMatch = text.match(/@(\w[\w.]*)$/);
|
||||
|
||||
if (!atMatch) {
|
||||
dropdown.classList.remove('show');
|
||||
mentionStart = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
mentionStart = pos - atMatch[0].length;
|
||||
const query = atMatch[1];
|
||||
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
fetch('/api/utenti/?q=' + encodeURIComponent(query))
|
||||
.then(r => r.json())
|
||||
.then(users => {
|
||||
if (!users.length) {
|
||||
dropdown.classList.remove('show');
|
||||
return;
|
||||
}
|
||||
dropdown.innerHTML = users.map(u =>
|
||||
`<div class="mention-item" data-username="${u.username}" data-nome="${u.nome}">
|
||||
<span class="avatar" style="width:22px;height:22px;font-size:.6rem;">${u.username.substring(0,2).toUpperCase()}</span>
|
||||
<span>${u.nome}</span>
|
||||
<small class="text-muted">@${u.username}</small>
|
||||
</div>`
|
||||
).join('');
|
||||
dropdown.classList.add('show');
|
||||
});
|
||||
}, 200);
|
||||
});
|
||||
|
||||
dropdown.addEventListener('click', (e) => {
|
||||
const item = e.target.closest('.mention-item');
|
||||
if (!item) return;
|
||||
const username = item.dataset.username;
|
||||
const before = textarea.value.substring(0, mentionStart);
|
||||
const after = textarea.value.substring(textarea.selectionStart);
|
||||
textarea.value = before + '@' + username + ' ' + after;
|
||||
textarea.focus();
|
||||
const newPos = mentionStart + username.length + 2;
|
||||
textarea.setSelectionRange(newPos, newPos);
|
||||
dropdown.classList.remove('show');
|
||||
});
|
||||
|
||||
textarea.addEventListener('blur', () => {
|
||||
setTimeout(() => dropdown.classList.remove('show'), 200);
|
||||
});
|
||||
|
||||
textarea.addEventListener('keydown', (e) => {
|
||||
if (!dropdown.classList.contains('show')) return;
|
||||
const items = dropdown.querySelectorAll('.mention-item');
|
||||
const active = dropdown.querySelector('.mention-item.active');
|
||||
let idx = Array.from(items).indexOf(active);
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (active) active.classList.remove('active');
|
||||
idx = (idx + 1) % items.length;
|
||||
items[idx].classList.add('active');
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (active) active.classList.remove('active');
|
||||
idx = idx <= 0 ? items.length - 1 : idx - 1;
|
||||
items[idx].classList.add('active');
|
||||
} else if (e.key === 'Enter' && active) {
|
||||
e.preventDefault();
|
||||
active.click();
|
||||
} else if (e.key === 'Escape') {
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Run on initial page load
|
||||
|
||||
Reference in New Issue
Block a user