手搓ag-grid带筛选的下拉选择器(类似企业版的agRichSelectCellEditor)

这两天有个项目需要用到ag-grid,有个需求要实现编辑单元格时出现下拉选项。如图所示

但是ag-grid的社区版只提供简单的下拉选项(agSelectCellEditor),只能在出现的下拉选项中选,而不能通过输入的方式筛选选项。

只有企业版的下拉选项(agRichSelectCellEditor)才提供输入和筛选功能。

所以这里手搓了一个自定义的编辑器组件,主要功能如下:

  • ✅ 输入筛选下拉选项

  • ✅ 首次打开显示全部选项

  • ✅ 键盘上下选择、回车选择

  • ✅ 只能选择可选项,非法输入时不更新

  • ✅ 点击组件外部时自动关闭

主要代码有两部分:js和css

js 复制代码
class RichSelectEditorWithFilter {
      init(params) {
        this.params = params;
        this.originalOptions = params.values || [];
        this.filteredOptions = [...this.originalOptions];
        this.selectedIndex = 0;
        this.initialValue = params.value; // 用于回退非法输入

        this.eGui = document.createElement('div');
        this.eGui.className = 'custom-rich-select';
        this.eGui.tabIndex = 0;

        // 输入框
        this.input = document.createElement('input');
        this.input.type = 'text';
        this.input.className = 'dropdown-input';
        this.input.value = params.value || '';
        this.hasUserTyped = false;

        this.input.addEventListener('input', () => {
          this.hasUserTyped = true;
          this.filterOptions();
        });

        this.input.addEventListener('keydown', (e) => this.handleKeyDown(e));

        this.dropdown = document.createElement('ul');
        this.dropdown.className = 'custom-rich-select-dropdown';

        this.eGui.appendChild(this.input);
        this.eGui.appendChild(this.dropdown);

        // 初始渲染所有选项
        this.renderOptions();

        // 添加全局点击事件监听器
        this.clickListener = (event) => {
          if (!this.eGui.contains(event.target)) {
            this.params.stopEditing(); // 点击编辑器外部时停止编辑
          }
        };
        document.addEventListener('mousedown', this.clickListener);
      }

      filterOptions() {
        const keyword = this.input.value.toLowerCase();

        if (!this.hasUserTyped || keyword === '') {
          this.filteredOptions = [...this.originalOptions];
        } else {
          this.filteredOptions = this.originalOptions.filter(opt =>
            opt.toLowerCase().includes(keyword)
          );
        }

        this.selectedIndex = 0;
        this.renderOptions();
      }

      renderOptions() {
        this.dropdown.innerHTML = '';

        this.filteredOptions.forEach((option, index) => {
          const item = document.createElement('li');
          item.textContent = option;
          item.className = 'dropdown-item';
          if (index === this.selectedIndex) {
            item.classList.add('selected');
          }

          item.addEventListener('mousedown', () => {
            this.input.value = option;
            this.params.stopEditing();
          });

          this.dropdown.appendChild(item);
        });
      }

      handleKeyDown(e) {
        if (e.key === 'ArrowDown') {
          this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredOptions.length - 1);
          this.renderOptions();
          e.preventDefault();
        } else if (e.key === 'ArrowUp') {
          this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
          this.renderOptions();
          e.preventDefault();
        } else if (e.key === 'Enter') {
          if (this.filteredOptions.length > 0) {
            this.input.value = this.filteredOptions[this.selectedIndex];
          }
          this.params.stopEditing();
        } else if (e.key === 'Escape') {
          this.params.stopEditing(true);
        }
      }

      getGui() {
        return this.eGui;
      }

      afterGuiAttached() {
        this.input.focus();
        this.input.select();
      }

      getValue() {
        const value = this.input.value;
        // 只有值合法(在可选项中)才返回,否则返回原值
        if (this.originalOptions.includes(value)) {
          return value;
        } else {
          return this.initialValue;
        }
      }

      isPopup() {
        return true;
      }

      destroy() {
        // 移除全局点击事件监听器
        document.removeEventListener('mousedown', this.clickListener);
      }
    }

样式文件:

css 复制代码
.custom-rich-select {
    border: 1px solid #ccc;
    background: white;
    width: 100%;
    font-family: sans-serif;
    font-size: 14px;
    outline: none;
    padding: 4px;
    box-sizing: border-box;
  }

  .dropdown-input {
    width: 100%;
    box-sizing: border-box;
    padding: 4px;
    margin-bottom: 4px;
    font-size: 14px;
  }

  .custom-rich-select-dropdown {
    list-style: none;
    margin: 0;
    padding: 0;
    max-height: 150px;
    overflow-y: auto;
    border-top: 1px solid #ddd;
  }

  .dropdown-item {
    padding: 6px 10px;
    cursor: pointer;
  }

  .dropdown-item.selected {
    background-color: #007acc;
    color: white;
  }

  .dropdown-item:hover {
    background-color: #cce5ff;
  }

完整html演示代码

html 复制代码
<html lang="en">

<head>
  <!-- Includes all JS & CSS for the JavaScript Data Grid -->
  <!-- ag-grid-enterprise.min.js -->
  <script src="https://cdn.jsdelivr.net/npm/ag-grid-community/dist/ag-grid-community.min.js"></script>
  <!-- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ag-grid-enterprise.min.js"></script> -->
</head>

<style>
  .custom-rich-select {
    border: 1px solid #ccc;
    background: white;
    width: 100%;
    font-family: sans-serif;
    font-size: 14px;
    outline: none;
    padding: 4px;
    box-sizing: border-box;
  }

  .dropdown-input {
    width: 100%;
    box-sizing: border-box;
    padding: 4px;
    margin-bottom: 4px;
    font-size: 14px;
  }

  .custom-rich-select-dropdown {
    list-style: none;
    margin: 0;
    padding: 0;
    max-height: 150px;
    overflow-y: auto;
    border-top: 1px solid #ddd;
  }

  .dropdown-item {
    padding: 6px 10px;
    cursor: pointer;
  }

  .dropdown-item.selected {
    background-color: #007acc;
    color: white;
  }

  .dropdown-item:hover {
    background-color: #cce5ff;
  }
</style>


<body>
  <!-- Your Data Grid container -->
  <div id="myGrid" style="height: 500px"></div>
  <script>
    let gridApi;

    class RichSelectEditorWithFilter {
      init(params) {
        this.params = params;
        this.originalOptions = params.values || [];
        this.filteredOptions = [...this.originalOptions];
        this.selectedIndex = 0;
        this.initialValue = params.value; // 用于回退非法输入

        this.eGui = document.createElement('div');
        this.eGui.className = 'custom-rich-select';
        this.eGui.tabIndex = 0;

        // 输入框
        this.input = document.createElement('input');
        this.input.type = 'text';
        this.input.className = 'dropdown-input';
        this.input.value = params.value || '';
        this.hasUserTyped = false;

        this.input.addEventListener('input', () => {
          this.hasUserTyped = true;
          this.filterOptions();
        });

        this.input.addEventListener('keydown', (e) => this.handleKeyDown(e));

        this.dropdown = document.createElement('ul');
        this.dropdown.className = 'custom-rich-select-dropdown';

        this.eGui.appendChild(this.input);
        this.eGui.appendChild(this.dropdown);

        // 初始渲染所有选项
        this.renderOptions();

        // 添加全局点击事件监听器
        this.clickListener = (event) => {
          if (!this.eGui.contains(event.target)) {
            this.params.stopEditing(); // 点击编辑器外部时停止编辑
          }
        };
        document.addEventListener('mousedown', this.clickListener);
      }

      filterOptions() {
        const keyword = this.input.value.toLowerCase();

        if (!this.hasUserTyped || keyword === '') {
          this.filteredOptions = [...this.originalOptions];
        } else {
          this.filteredOptions = this.originalOptions.filter(opt =>
            opt.toLowerCase().includes(keyword)
          );
        }

        this.selectedIndex = 0;
        this.renderOptions();
      }

      renderOptions() {
        this.dropdown.innerHTML = '';

        this.filteredOptions.forEach((option, index) => {
          const item = document.createElement('li');
          item.textContent = option;
          item.className = 'dropdown-item';
          if (index === this.selectedIndex) {
            item.classList.add('selected');
          }

          item.addEventListener('mousedown', () => {
            this.input.value = option;
            this.params.stopEditing();
          });

          this.dropdown.appendChild(item);
        });
      }

      handleKeyDown(e) {
        if (e.key === 'ArrowDown') {
          this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredOptions.length - 1);
          this.renderOptions();
          e.preventDefault();
        } else if (e.key === 'ArrowUp') {
          this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
          this.renderOptions();
          e.preventDefault();
        } else if (e.key === 'Enter') {
          if (this.filteredOptions.length > 0) {
            this.input.value = this.filteredOptions[this.selectedIndex];
          }
          this.params.stopEditing();
        } else if (e.key === 'Escape') {
          this.params.stopEditing(true);
        }
      }

      getGui() {
        return this.eGui;
      }

      afterGuiAttached() {
        this.input.focus();
        this.input.select();
      }

      getValue() {
        const value = this.input.value;
        // 只有值合法(在可选项中)才返回,否则返回原值
        if (this.originalOptions.includes(value)) {
          return value;
        } else {
          return this.initialValue;
        }
      }

      isPopup() {
        return true;
      }

      destroy() {
        // 移除全局点击事件监听器
        document.removeEventListener('mousedown', this.clickListener);
      }
    }


    const categoryList = [
      "小碗菜", "米线", "火锅", "串串", "小吃", "金汤米线"
    ]

    const gridOptions = {
      // Data to be displayed
      rowData: [
        { category: "小碗菜", name: '宫保鸡丁', foodFeature: '', dishFeature: '', publishCategory: '热销菜品', publishName: '特价宫保鸡丁' },

      ],
      // Columns to be displayed (Should match rowData properties)
      columnDefs: [
        {
          headerName: '品类', field: 'category',
          editable: true,
          cellEditor: RichSelectEditorWithFilter,
          cellEditorParams: {
            values: categoryList,
          },
          cellEditorPopup: true, // 让下拉框浮动显示
        },
        { headerName: '菜品名称', field: 'name' },
        { headerName: '菜品特色', field: 'foodFeature' },
        { headerName: '菜品描述', field: 'dishFeature' },
        { headerName: '发布品类', field: 'publishCategory' },
        { headerName: '发布名称', field: 'publishName' },
      ],
    };

    const gridDiv = document.querySelector("#myGrid");
    gridApi = agGrid.createGrid(gridDiv, gridOptions);

  </script>
</body>

</html>

希望以上代码能够帮到你,顺便点个赞!!!

相关推荐
2501_915373883 小时前
Vue 3零基础入门:从环境搭建到第一个组件
前端·javascript·vue.js
沙振宇5 小时前
【Web】使用Vue3开发鸿蒙的HelloWorld!
前端·华为·harmonyos
运维@小兵6 小时前
vue开发用户注册功能
前端·javascript·vue.js
蓝婷儿6 小时前
前端面试每日三题 - Day 30
前端·面试·职场和发展
oMMh6 小时前
使用C# ASP.NET创建一个可以由服务端推送信息至客户端的WEB应用(2)
前端·c#·asp.net
一口一个橘子7 小时前
[ctfshow web入门] web69
前端·web安全·网络安全
读心悦8 小时前
CSS:盒子阴影与渐变完全解析:从基础语法到创意应用
前端·css
湛海不过深蓝9 小时前
【ts】defineProps数组的类型声明
前端·javascript·vue.js
layman05289 小时前
vue 中的数据代理
前端·javascript·vue.js
柒七爱吃麻辣烫9 小时前
前端项目打包部署流程j
前端