这两天有个项目需要用到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>
希望以上代码能够帮到你,顺便点个赞!!!