手搓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/ag-grid-enterprise@33.2.3/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>

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

相关推荐
孤水寒月3 小时前
基于HTML的悬窗可拖动记事本
前端·css·html
祝余呀3 小时前
html初学者第一天
前端·html
耶啵奶膘6 小时前
uniapp+firstUI——上传视频组件fui-upload-video
前端·javascript·uni-app
视频砖家6 小时前
移动端Html5播放器按钮变小的问题解决方法
前端·javascript·viewport功能
lyj1689977 小时前
vue-i18n+vscode+vue 多语言使用
前端·vue.js·vscode
小白变怪兽8 小时前
一、react18+项目初始化(vite)
前端·react.js
ai小鬼头8 小时前
AIStarter如何快速部署Stable Diffusion?**新手也能轻松上手的AI绘图
前端·后端·github
墨菲安全9 小时前
NPM组件 betsson 等窃取主机敏感信息
前端·npm·node.js·软件供应链安全·主机信息窃取·npm组件投毒
GISer_Jing9 小时前
Monorepo+Pnpm+Turborepo
前端·javascript·ecmascript
天涯学馆9 小时前
前端开发也能用 WebAssembly?这些场景超实用!
前端·javascript·面试