如何在输入框输入@后调出下拉列表?JavaScript实现方法咨询
Hey there! Let's break down how to build that WhatsApp-style @mention dropdown for group chats. I've put together a practical, step-by-step implementation using vanilla JavaScript—no heavy libraries needed, so you can drop this into your project easily.
To replicate WhatsApp's behavior, we need to handle these key parts:
- Detect when the user types an
@character in the input area - Show a dropdown list of group members right below the cursor
- Filter the list as the user types characters after
@(e.g.,@Joshows only members starting with "Jo") - Insert the selected member's name into the input when clicked
- Hide the dropdown when clicking outside, pressing ESC, or moving the cursor away from the mention
1. HTML Structure
We'll use a contenteditable div for the input area (this mimics WhatsApp's chat input better than a standard textarea, since it supports inline mentions). We'll also add a container for the dropdown list:
<div class="chat-input-container"> <div id="chatInput" contenteditable="true" placeholder="Type a message..."></div> <div id="mentionDropdown" class="mention-dropdown"></div> </div>
2. CSS Styling
Let's style this to match WhatsApp's clean, minimal look:
.chat-input-container { position: relative; width: 90%; max-width: 600px; margin: 20px auto; } #chatInput { padding: 12px 16px; border: 1px solid #ddd; border-radius: 24px; min-height: 40px; font-size: 16px; font-family: sans-serif; } #chatInput:focus { outline: none; border-color: #25d366; /* WhatsApp green */ } .mention-dropdown { position: absolute; top: 100%; left: 0; right: 0; margin-top: 8px; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.15); max-height: 240px; overflow-y: auto; display: none; z-index: 100; } .mention-item { display: flex; align-items: center; padding: 10px 12px; cursor: pointer; gap: 10px; } .mention-item:hover { background: #f0f2f5; /* Light gray hover */ } .mention-avatar { width: 36px; height: 36px; border-radius: 50%; background: #25d366; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; } .mention-name { font-size: 15px; font-family: sans-serif; }
3. JavaScript Logic
Now let's add the functionality. We'll start with sample group member data, then build out each part:
// Sample group members (replace with your actual data) const groupMembers = [ { id: 1, name: "Alice Smith", avatar: "A" }, { id: 2, name: "Bob Johnson", avatar: "B" }, { id: 3, name: "Charlie Davis", avatar: "C" }, { id: 4, name: "Diana Evans", avatar: "D" }, { id: 5, name: "Ethan Wilson", avatar: "E" } ]; // Get DOM elements const chatInput = document.getElementById('chatInput'); const mentionDropdown = document.getElementById('mentionDropdown'); // Track current mention query (text after @) let currentMentionQuery = ''; let isMentionActive = false; // Listen for input events in the chat box chatInput.addEventListener('input', handleInput); // Listen for clicks outside to close dropdown document.addEventListener('click', handleOutsideClick); // Listen for ESC key to close dropdown document.addEventListener('keydown', handleKeydown); function handleInput(e) { const cursorPosition = getCursorPosition(); const textBeforeCursor = chatInput.innerText.substring(0, cursorPosition); // Check if we're in a mention state (last character before cursor is @, or we're typing after @) const mentionMatch = textBeforeCursor.match(/@([a-zA-Z0-9_]*)$/); if (mentionMatch) { isMentionActive = true; currentMentionQuery = mentionMatch[1]; showMentionDropdown(mentionMatch[0], cursorPosition); } else { isMentionActive = false; hideMentionDropdown(); } } function showMentionDropdown(fullMentionText, cursorPosition) { // Filter members based on the query const filteredMembers = groupMembers.filter(member => member.name.toLowerCase().includes(currentMentionQuery.toLowerCase()) ); if (filteredMembers.length === 0) { hideMentionDropdown(); return; } // Render the dropdown items mentionDropdown.innerHTML = filteredMembers.map(member => ` <div class="mention-item" data-id="${member.id}"> <div class="mention-avatar">${member.avatar}</div> <div class="mention-name">${member.name}</div> </div> `).join(''); // Position the dropdown near the cursor (optional, for better UX) const range = document.createRange(); const selection = window.getSelection(); selection.collapse(chatInput.firstChild, cursorPosition - fullMentionText.length); range.setStart(selection.anchorNode, selection.anchorOffset); range.setEnd(selection.anchorNode, selection.anchorOffset); const rect = range.getBoundingClientRect(); const containerRect = chatInput.getBoundingClientRect(); mentionDropdown.style.left = `${rect.left - containerRect.left}px`; mentionDropdown.style.width = `${Math.max(200, rect.width + 40)}px`; mentionDropdown.style.display = 'block'; // Add click handlers to each item document.querySelectorAll('.mention-item').forEach(item => { item.addEventListener('click', () => selectMember(item.dataset.id)); }); } function hideMentionDropdown() { mentionDropdown.style.display = 'none'; isMentionActive = false; currentMentionQuery = ''; } function selectMember(memberId) { const member = groupMembers.find(m => m.id === parseInt(memberId)); if (!member) return; // Get the current mention text to replace const textBeforeCursor = chatInput.innerText.substring(0, getCursorPosition()); const mentionMatch = textBeforeCursor.match(/@([a-zA-Z0-9_]*)$/); if (!mentionMatch) return; // Replace the mention query with the full @name const newText = textBeforeCursor.replace(mentionMatch[0], `@${member.name} `); chatInput.innerText = newText + chatInput.innerText.substring(getCursorPosition()); // Move cursor to the end of the inserted mention setCursorPosition(newText.length); hideMentionDropdown(); } function handleOutsideClick(e) { if (!chatInput.contains(e.target) && !mentionDropdown.contains(e.target)) { hideMentionDropdown(); } } function handleKeydown(e) { if (e.key === 'Escape') { hideMentionDropdown(); } // Optional: Add arrow key navigation for the dropdown if (isMentionActive && mentionDropdown.style.display === 'block') { const items = Array.from(document.querySelectorAll('.mention-item')); const activeItem = items.find(item => item.classList.contains('active')); let index = activeItem ? items.indexOf(activeItem) : -1; if (e.key === 'ArrowDown') { e.preventDefault(); index = (index + 1) % items.length; } else if (e.key === 'ArrowUp') { e.preventDefault(); index = index <= 0 ? items.length - 1 : index - 1; } else if (e.key === 'Enter' && activeItem) { e.preventDefault(); selectMember(activeItem.dataset.id); return; } items.forEach(item => item.classList.remove('active')); items[index].classList.add('active'); items[index].scrollIntoView({ block: 'nearest' }); } } // Helper: Get current cursor position in contenteditable function getCursorPosition() { const selection = window.getSelection(); if (selection.rangeCount === 0) return 0; const range = selection.getRangeAt(0); const preCaretRange = range.cloneRange(); preCaretRange.selectNodeContents(chatInput); preCaretRange.setEnd(range.endContainer, range.endOffset); return preCaretRange.toString().length; } // Helper: Set cursor position in contenteditable function setCursorPosition(pos) { const range = document.createRange(); const selection = window.getSelection(); range.setStart(chatInput.firstChild, pos); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); chatInput.focus(); }
- Avatar Integration: Replace the placeholder avatars with actual user profile images if you have them
- Case Insensitivity: The filter already handles this, but you could tweak it to prioritize exact matches first
- Mobile Responsiveness: Adjust the CSS to make the dropdown look good on small screens
- Mention Highlighting: Add a background color to inserted mentions (like WhatsApp's light blue) by wrapping them in a
<span>with a class - Multiple Mentions: The current code supports multiple mentions in one message—test it out!
- If the cursor position logic acts up, test with different contenteditable states (empty text, text with line breaks)
- For mobile devices, ensure the dropdown doesn't get cut off by the screen edge (you can adjust the
top/leftpositioning dynamically) - If using a textarea instead of contenteditable, you'll need to adjust the cursor position logic since textareas handle selections differently
内容的提问来源于stack exchange,提问作者Ravindra Engle

