You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在输入框输入@后调出下拉列表?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.

Core Concepts We'll Cover

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., @Jo shows 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
Step-by-Step Implementation

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();
}
Key Enhancements to Match WhatsApp Perfectly
  • 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!
Troubleshooting Tips
  • 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/left positioning 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:45:26