(function(root, factory) { const api = factory(); if (typeof module === 'object' && module.exports) module.exports = api; else root.createMentionComposer = api.create; })(typeof globalThis !== 'undefined' ? globalThis : this, function() { 'use strict'; function activeMention(value, caret) { const before = String(value || '').slice(0, Math.max(0, caret)); const match = before.match(/(^|\s)@([A-Za-z0-9_.-]{2,39})$/); if (!match) return null; const start = before.length - match[2].length - 1; return { start, end:before.length, query:match[2] }; } function insertMention(value, mention, login) { const text = String(value || ''); const suffixStart = mention.end < text.length && /\s/.test(text[mention.end]) ? mention.end + 1 : mention.end; const inserted = '@' + login + ' '; return { value:text.slice(0, mention.start) + inserted + text.slice(suffixStart), caret:mention.start + inserted.length, }; } function create(options) { const textarea = options.textarea; const listbox = options.listbox; const status = options.status; const setTimer = options.setTimer || setTimeout; const clearTimer = options.clearTimer || clearTimeout; const createOption = options.createOption || (() => document.createElement('button')); let timer = null; let requestId = 0; let mention = null; let candidates = []; let activeIndex = -1; function dismiss() { requestId += 1; candidates = []; activeIndex = -1; listbox.replaceChildren(); listbox.hidden = true; textarea.setAttribute('aria-expanded', 'false'); textarea.removeAttribute('aria-activedescendant'); status.textContent = ''; } function activate(index) { if (!candidates.length) return; activeIndex = (index + candidates.length) % candidates.length; listbox.children.forEach((option, optionIndex) => { option.setAttribute('aria-selected', optionIndex === activeIndex ? 'true' : 'false'); }); textarea.setAttribute('aria-activedescendant', listbox.children[activeIndex].id); } function select(index) { if (!mention || !candidates[index]) return; const result = insertMention(textarea.value, mention, candidates[index].login); textarea.value = result.value; textarea.setSelectionRange(result.caret, result.caret); dismiss(); textarea.focus(); textarea.dispatchEvent?.(new Event('input', { bubbles:true })); } function render(items) { candidates = Array.isArray(items) ? items : []; activeIndex = -1; listbox.replaceChildren(); candidates.forEach((candidate, index) => { const option = createOption(); option.type = 'button'; option.id = listbox.id + '-option-' + index; option.className = 'mention-option'; option.dataset.login = candidate.login; option.setAttribute('role', 'option'); option.setAttribute('aria-selected', 'false'); option.textContent = '@' + candidate.login + (candidate.name !== candidate.login ? ' ยท ' + candidate.name : ''); option.addEventListener('pointerdown', event => { event.preventDefault(); select(index); }); listbox.appendChild(option); }); listbox.hidden = !candidates.length; textarea.setAttribute('aria-expanded', candidates.length ? 'true' : 'false'); status.textContent = candidates.length ? candidates.length + ' teammates found.' : 'No matching teammates.'; } function schedule() { if (timer !== null) clearTimer(timer); mention = activeMention(textarea.value, textarea.selectionStart); const repository = options.getRepository(); if (!mention || !repository) { dismiss(); return; } const currentRequest = ++requestId; timer = setTimer(async () => { try { const items = await options.loadCandidates(repository, mention.query); if (currentRequest === requestId && repository === options.getRepository()) render(items); } catch (_error) { if (currentRequest === requestId) { listbox.hidden = true; textarea.setAttribute('aria-expanded', 'false'); status.textContent = 'Teammate suggestions unavailable; you can keep typing.'; } } }, options.delayMs ?? 180); } function keydown(event) { if (event.key === 'Escape' && !listbox.hidden) { event.preventDefault(); dismiss(); } else if (event.key === 'ArrowDown' && candidates.length) { event.preventDefault(); activate(activeIndex + 1); } else if (event.key === 'ArrowUp' && candidates.length) { event.preventDefault(); activate(activeIndex < 0 ? candidates.length - 1 : activeIndex - 1); } else if (event.key === 'Enter' && activeIndex >= 0) { event.preventDefault(); select(activeIndex); } } return { start() { textarea.setAttribute('autocomplete', 'off'); textarea.setAttribute('aria-autocomplete', 'list'); textarea.setAttribute('aria-controls', listbox.id); textarea.setAttribute('aria-expanded', 'false'); textarea.addEventListener('input', schedule); textarea.addEventListener('click', schedule); textarea.addEventListener('keydown', keydown); }, dismiss, }; } return { activeMention, insertMention, create }; });