|

JavaScript/CSS 下拉菜单搜索功能

实现一个带搜索功能的下拉菜单(Dropdown with Search)是 Web 开发中常见的交互需求,允许用户从下拉列表中选择选项,并通过输入关键字过滤选项。以下是详细的中文讲解,介绍如何使用 JavaScript 和 CSS 实现下拉菜单搜索功能,包含完整代码示例、样式美化、功能说明和注意事项。


1. 功能需求

  • 目标:创建一个可搜索的下拉菜单,用户可以:
  • 点击显示/隐藏下拉列表。
  • 在输入框中键入关键字,实时过滤选项。
  • 选择选项后显示选中值。
  • 功能
  • 输入框支持动态过滤(大小写不敏感)。
  • 键盘和鼠标交互支持。
  • 样式美观,响应式设计。
  • 视觉反馈(如高亮选中项)。
  • 技术
  • JavaScript:处理输入事件、过滤逻辑、DOM 操作。
  • CSS:美化下拉菜单、输入框和选项样式。

2. 实现步骤

2.1 HTML 结构

  • 使用 <div> 包装下拉菜单,包含输入框和选项列表。
  • 选项使用 <ul><li> 实现,支持动态过滤。
  • 示例结构:
  <div class="dropdown">
      <input type="text" class="search-input" placeholder="搜索...">
      <ul class="dropdown-list">
          <li data-value="apple">Apple</li>
          <li data-value="banana">Banana</li>
          <li data-value="cherry">Cherry</li>
          <li data-value="date">Date</li>
          <li data-value="grape">Grape</li>
      </ul>
      <div class="selected-value">未选择</div>
  </div>
  • 说明
    • data-value 存储选项值,便于程序处理。
    • .selected-value 显示当前选中项。
    • <input> 用于搜索过滤。

2.2 CSS 样式

  • 美化输入框、下拉列表和选项。
  • 添加交互效果(如悬停、选中高亮、隐藏/显示列表)。
  • 示例 CSS:
  .dropdown {
      position: relative;
      width: 300px;
      margin: 20px auto;
      font-family: Arial, sans-serif;
  }
  .search-input {
      width: 100%;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 16px;
      box-sizing: border-box;
  }
  .search-input:focus {
      outline: none;
      border-color: #007bff;
      box-shadow: 0 0 5px rgba(0, 123, 255, 0.3);
  }
  .dropdown-list {
      list-style: none;
      padding: 0;
      margin: 0;
      border: 1px solid #ddd;
      border-radius: 4px;
      max-height: 200px;
      overflow-y: auto;
      position: absolute;
      width: 100%;
      background: #fff;
      display: none;
      z-index: 1000;
  }
  .dropdown-list.show {
      display: block;
  }
  .dropdown-list li {
      padding: 10px;
      cursor: pointer;
      transition: background 0.2s;
  }
  .dropdown-list li:hover {
      background: #f0f0f0;
  }
  .dropdown-list li.selected {
      background: #007bff;
      color: white;
  }
  .selected-value {
      margin-top: 10px;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 4px;
      background: #f9f9f9;
  }
  • 说明
    • .dropdown 使用相对定位,.dropdown-list 使用绝对定位实现下拉效果。
    • max-heightoverflow-y 限制列表高度,添加滚动条。
    • .show 类控制列表显示/隐藏。
    • 交互样式:输入框聚焦、选项悬停和选中效果。

2.3 JavaScript 逻辑

  • 逻辑
  1. 输入框监听输入事件,实时过滤选项。
  2. 点击输入框显示下拉列表,点击外部隐藏。
  3. 点击选项更新选中值并隐藏列表。
  4. 支持键盘导航(上/下箭头、Enter 选择)。
  • 代码示例
  document.addEventListener('DOMContentLoaded', () => {
      const dropdown = document.querySelector('.dropdown');
      const input = dropdown.querySelector('.search-input');
      const list = dropdown.querySelector('.dropdown-list');
      const items = list.querySelectorAll('li');
      const selectedValue = dropdown.querySelector('.selected-value');
      let focusedIndex = -1;

      // 显示/隐藏下拉列表
      input.addEventListener('focus', () => {
          list.classList.add('show');
          filterItems();
      });

      // 点击外部隐藏列表
      document.addEventListener('click', (e) => {
          if (!dropdown.contains(e.target)) {
              list.classList.remove('show');
              focusedIndex = -1;
              updateFocus();
          }
      });

      // 过滤选项
      input.addEventListener('input', filterItems);
      function filterItems() {
          const query = input.value.toLowerCase();
          items.forEach(item => {
              const text = item.textContent.toLowerCase();
              item.style.display = text.includes(query) ? '' : 'none';
          });
          focusedIndex = -1;
          updateFocus();
      }

      // 选择选项
      items.forEach((item, index) => {
          item.addEventListener('click', () => {
              selectedValue.textContent = item.textContent;
              input.value = item.textContent;
              list.classList.remove('show');
              focusedIndex = -1;
              updateFocus();
          });
      });

      // 键盘导航
      input.addEventListener('keydown', (e) => {
          const visibleItems = Array.from(items).filter(item => item.style.display !== 'none');
          if (e.key === 'ArrowDown') {
              e.preventDefault();
              focusedIndex = Math.min(focusedIndex + 1, visibleItems.length - 1);
              updateFocus();
          } else if (e.key === 'ArrowUp') {
              e.preventDefault();
              focusedIndex = Math.max(focusedIndex - 1, -1);
              updateFocus();
          } else if (e.key === 'Enter' && focusedIndex >= 0) {
              e.preventDefault();
              const selectedItem = visibleItems[focusedIndex];
              selectedValue.textContent = selectedItem.textContent;
              input.value = selectedItem.textContent;
              list.classList.remove('show');
              focusedIndex = -1;
              updateFocus();
          }
      });

      // 更新焦点样式
      function updateFocus() {
          items.forEach((item, index) => {
              item.classList.remove('selected');
              if (index === focusedIndex && item.style.display !== 'none') {
                  item.classList.add('selected');
                  item.scrollIntoView({ block: 'nearest' });
              }
          });
      }
  });

3. 完整代码示例

以下是一个完整的 HTML 文件,包含 HTML、CSS 和 JavaScript,实现带搜索功能的下拉菜单:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>下拉菜单搜索</title>
    <style>
        .dropdown {
            position: relative;
            width: 300px;
            margin: 20px auto;
            font-family: Arial, sans-serif;
        }
        .search-input {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 16px;
            box-sizing: border-box;
        }
        .search-input:focus {
            outline: none;
            border-color: #007bff;
            box-shadow: 0 0 5px rgba(0, 123, 255, 0.3);
        }
        .dropdown-list {
            list-style: none;
            padding: 0;
            margin: 0;
            border: 1px solid #ddd;
            border-radius: 4px;
            max-height: 200px;
            overflow-y: auto;
            position: absolute;
            width: 100%;
            background: #fff;
            display: none;
            z-index: 1000;
        }
        .dropdown-list.show {
            display: block;
        }
        .dropdown-list li {
            padding: 10px;
            cursor: pointer;
            transition: background 0.2s;
        }
        .dropdown-list li:hover {
            background: #f0f0f0;
        }
        .dropdown-list li.selected {
            background: #007bff;
            color: white;
        }
        .selected-value {
            margin-top: 10px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            background: #f9f9f9;
        }
    </style>
</head>
<body>
    <div class="dropdown">
        <input type="text" class="search-input" placeholder="搜索...">
        <ul class="dropdown-list">
            <li data-value="apple">Apple</li>
            <li data-value="banana">Banana</li>
            <li data-value="cherry">Cherry</li>
            <li data-value="date">Date</li>
            <li data-value="grape">Grape</li>
            <li data-value="orange">Orange</li>
            <li data-value="peach">Peach</li>
        </ul>
        <div class="selected-value">未选择</div>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const dropdown = document.querySelector('.dropdown');
            const input = dropdown.querySelector('.search-input');
            const list = dropdown.querySelector('.dropdown-list');
            const items = list.querySelectorAll('li');
            const selectedValue = dropdown.querySelector('.selected-value');
            let focusedIndex = -1;

            // 显示/隐藏下拉列表
            input.addEventListener('focus', () => {
                list.classList.add('show');
                filterItems();
            });

            // 点击外部隐藏列表
            document.addEventListener('click', (e) => {
                if (!dropdown.contains(e.target)) {
                    list.classList.remove('show');
                    focusedIndex = -1;
                    updateFocus();
                }
            });

            // 过滤选项
            input.addEventListener('input', filterItems);
            function filterItems() {
                const query = input.value.toLowerCase();
                items.forEach(item => {
                    const text = item.textContent.toLowerCase();
                    item.style.display = text.includes(query) ? '' : 'none';
                });
                focusedIndex = -1;
                updateFocus();
            }

            // 选择选项
            items.forEach((item, index) => {
                item.addEventListener('click', () => {
                    selectedValue.textContent = item.textContent;
                    input.value = item.textContent;
                    list.classList.remove('show');
                    focusedIndex = -1;
                    updateFocus();
                });
            });

            // 键盘导航
            input.addEventListener('keydown', (e) => {
                const visibleItems = Array.from(items).filter(item => item.style.display !== 'none');
                if (e.key === 'ArrowDown') {
                    e.preventDefault();
                    focusedIndex = Math.min(focusedIndex + 1, visibleItems.length - 1);
                    updateFocus();
                } else if (e.key === 'ArrowUp') {
                    e.preventDefault();
                    focusedIndex = Math.max(focusedIndex - 1, -1);
                    updateFocus();
                } else if (e.key === 'Enter' && focusedIndex >= 0) {
                    e.preventDefault();
                    const selectedItem = visibleItems[focusedIndex];
                    selectedValue.textContent = selectedItem.textContent;
                    input.value = selectedItem.textContent;
                    list.classList.remove('show');
                    focusedIndex = -1;
                    updateFocus();
                }
            });

            // 更新焦点样式
            function updateFocus() {
                items.forEach((item, index) => {
                    item.classList.remove('selected');
                    if (index === focusedIndex && item.style.display !== 'none') {
                        item.classList.add('selected');
                        item.scrollIntoView({ block: 'nearest' });
                    }
                });
            }
        });
    </script>
</body>
</html>

4. 代码说明

  • HTML
  • .dropdown 包含输入框、选项列表和选中值显示。
  • <ul> 包含多个 <li>,每个 <li>data-value 属性。
  • .selected-value 显示当前选择。
  • CSS
  • 输入框:聚焦效果、边框圆角。
  • 下拉列表:绝对定位、滚动条、隐藏/显示控制。
  • 选项:悬停高亮、选中蓝色背景、平滑过渡。
  • JavaScript
  • 事件监听
    • 输入框 focus 显示列表。
    • input 事件实时过滤选项(大小写不敏感)。
    • 点击外部隐藏列表。
    • 点击选项更新选中值。
  • 过滤逻辑:使用 includes 匹配输入关键字,动态设置 display 属性。
  • 键盘导航
    • 上/下箭头切换焦点。
    • Enter 键确认选择。
    • scrollIntoView 确保选中项可见。
  • 焦点管理focusedIndex 跟踪键盘选中的选项,动态更新 .selected 样式。

5. jQuery 实现(可选)

如果项目已使用 jQuery,可以简化 DOM 操作。以下是 jQuery 版本的代码:

$(document).ready(function() {
    const $dropdown = $('.dropdown');
    const $input = $('.search-input');
    const $list = $('.dropdown-list');
    const $items = $list.find('li');
    const $selectedValue = $('.selected-value');
    let focusedIndex = -1;

    // 显示列表
    $input.on('focus', () => {
        $list.addClass('show');
        filterItems();
    });

    // 点击外部隐藏
    $(document).on('click', (e) => {
        if (!$dropdown.is(e.target) && !$dropdown.has(e.target).length) {
            $list.removeClass('show');
            focusedIndex = -1;
            updateFocus();
        }
    });

    // 过滤选项
    $input.on('input', filterItems);
    function filterItems() {
        const query = $input.val().toLowerCase();
        $items.each(function() {
            const text = $(this).text().toLowerCase();
            $(this).css('display', text.includes(query) ? '' : 'none');
        });
        focusedIndex = -1;
        updateFocus();
    }

    // 选择选项
    $items.on('click', function() {
        $selectedValue.text($(this).text());
        $input.val($(this).text());
        $list.removeClass('show');
        focusedIndex = -1;
        updateFocus();
    });

    // 键盘导航
    $input.on('keydown', function(e) {
        const $visibleItems = $items.filter(':visible');
        if (e.key === 'ArrowDown') {
            e.preventDefault();
            focusedIndex = Math.min(focusedIndex + 1, $visibleItems.length - 1);
            updateFocus();
        } else if (e.key === 'ArrowUp') {
            e.preventDefault();
            focusedIndex = Math.max(focusedIndex - 1, -1);
            updateFocus();
        } else if (e.key === 'Enter' && focusedIndex >= 0) {
            e.preventDefault();
            const $selectedItem = $visibleItems.eq(focusedIndex);
            $selectedValue.text($selectedItem.text());
            $input.val($selectedItem.text());
            $list.removeClass('show');
            focusedIndex = -1;
            updateFocus();
        }
    });

    function updateFocus() {
        $items.removeClass('selected');
        if (focusedIndex >= 0) {
            const $visibleItems = $items.filter(':visible');
            $visibleItems.eq(focusedIndex).addClass('selected').get(0).scrollIntoView({ block: 'nearest' });
        }
    }
});
  • 依赖:需引入 jQuery:
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

6. 方法对比

方法/特点描述优点缺点
原生 JavaScript使用 DOM 操作和事件处理实现无依赖,性能好,灵活代码稍复杂
jQuery使用 jQuery 简化 DOM 和事件处理代码简洁,适合 jQuery 项目需引入 jQuery,增加加载时间
第三方库(如 Select2)使用现成库实现搜索下拉功能丰富,开箱即用体积大,可能过度复杂

Select2 示例(简单配置):

<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<select class="search-select">
    <option value="apple">Apple</option>
    <option value="banana">Banana</option>
    <option value="cherry">Cherry</option>
</select>
<script>
    $(document).ready(function() {
        $('.search-select').select2();
    });
</script>
  • 说明:Select2 提供开箱即用的搜索下拉,但需额外库。

7. 注意事项

  • 输入过滤
  • 使用 toLowerCase 实现大小写不敏感。
  • 可扩展支持正则表达式或模糊匹配:
    javascript const regex = new RegExp(query, 'i'); item.style.display = regex.test(text) ? '' : 'none';
  • 性能
  • 小列表(<100 项):原生或 jQuery 性能足够。
  • 大列表(>1000 项):考虑虚拟化(如仅渲染可见选项)。
  • 浏览器兼容性
  • addEventListenerquerySelector 是 ES5 标准,现代浏览器全支持。
  • scrollIntoViewincludes 兼容性好,IE11 需 polyfill。
  • 用户体验
  • 键盘导航(箭头、Enter)增强可访问性。
  • 添加加载动画(动态数据时)。
  • 确保列表宽度与输入框一致。
  • 扩展性
  • 支持多选:添加 multiple 属性,调整逻辑存储多个值。
  • 动态数据:通过 AJAX 加载选项:
    javascript fetch('options.json').then(res => res.json()).then(data => { list.innerHTML = data.map(item => `<li data-value="${item.value}">${item.label}</li>`).join(''); });
  • 安全性
  • 用户输入需转义,防止 XSS:
    javascript const safeText = input.value.replace(/[<>]/g, '');

8. 总结

  • 实现方式:原生 JavaScript + CSS 实现轻量、灵活的搜索下拉菜单。
  • 核心逻辑
  • 输入框过滤选项(includes)。
  • 点击和键盘交互选择选项。
  • CSS 提供显示/隐藏和视觉反馈。
  • 替代方案
  • jQuery:简化 DOM 操作。
  • Select2:功能丰富,适合复杂需求。
  • 选择依据
  • 小型项目:原生 JavaScript。
  • jQuery 项目:使用 jQuery 简化代码。
  • 高级功能:考虑 Select2 或框架集成(如 React Select)。
  • 测试:验证过滤、选择、键盘导航,确保样式和交互一致。

如果需要扩展功能(如多选、动态加载、结合 React/Vue),或有其他问题,请提供更多细节,我可以进一步优化回答!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注