效果图:
思路
原本是想弄一个输入框input,挡在原生select的前面,结果发现,原生select无论怎么弄,都无法js手动控制展开下拉选,必须点击select,这就很尴尬
然后就只能弄一个输入框input,然后用ul生成下拉选,输入框聚焦打开下拉选,输入框失焦关闭下拉选,最后封装到一个自定义标签中实现了如图效果。
遇到问题
这里有一个问题,类似码值下拉选一般都是配合表单提交的,这里使用原生表单form的提交功能,我在自定义元素中提供了name属性,提供了value属性,还是无法让form提交时带上输入框的值。
因为搜不到解决方法,查看源码也找不到源码实现,因此目前是手动在同级位置插入一个隐藏的input标签,把name属性设置到input上,然后当value更新时同步更新input的值,这样表单提交时就会自动提交了,真是个大聪明
结果
我给它命名为
<auto-input-select></auto-input-select>
一共实现了两种效果,码值搜索(默认)和普通文本搜索
使用方式
使用方式很简单
(1)引入js脚本
<script src="auto-input-select.js"></script>
(2)页面上使用
<auto-input-select type="text" placeholder="请输入查询语言" data='["java","c++","python"]'></auto-input-select>
(3)提供js函数入口实现复杂功能
属性介绍
value | 设置值,当value变化时,会实时处理 |
data | 设置数据源,当data变化时,会实时处理 |
type | code默认(码值搜索),text(文本搜索) |
placeholder | 设置提示信息 |
style | 设置整体样式 |
input-style | 设置input样式 |
item-style | 设置下拉选样式 |
select-max-height | 设置下拉选框的最大高度,默认是输入框的8倍,展示8个选项,超出部分滚动展示 |
load-max-num | 选项加载最大数量,默认200 |
js函数介绍
函数 | 返回类型 | 描述 |
setData( String | Array , Function , Function ) | void | 设置下拉选数据源,Array字符串或者Array对象, 当type=code时:必须包含name和value 函数1:(item)=>{ return String ; } 返回value的值; 当数组项种没有value属性,可以传递该函数设置 函数2:(item)=>{ return String ; } 返回name的值; 当数组项种没有name属性,可以传递该函数设置 当type=text时:数组项不是String类型,会自动将内容转成String |
findData( any ) | 数组项 | 返回一个根据value查询到的数据项 |
setItemStyle( Function ) | void | (data,item,keyWord)=>{ return String ; } 设置样式回调函数,允许用户根据选项数据和搜索词自由设置选项展示效果,返回html代码字符串 |
setValue( any ) | void | 设置value值,也可以直接访问value属性设置 |
getValue() | value | 获取value值,也可以直接访问value属性获取 |
getKeyWord() | String | 获取输入框的搜索词 |
searchName( String ) | void | 允许调用该方式手动触发搜索功能,在下拉框展开的时候,可以看到下拉选项同步变化 |
open() | void | 允许手动打开下拉选框 |
close() | void | 允许手动关闭下拉选框 |
isOpen() | bool | 判断下拉框的打开状态 |
事件触发
input | 输入框的输入事件,可以使用标签属性oninput |
focus | 输入框的聚焦事件,可以使用标签属性onfocus |
blur | 输入框的失焦事件,可以使用标签属性onblur |
select | 下拉选选项变化事件,可以使用标签属性onselect |
open | 打开事件,标签属性onopen不管用,只能用addEventListener( "open" , Function ) |
close | 关闭事件,可以使用标签属性onclose |
源代码
(function () {
let tagName = 'auto-input-select';
if (customElements.get(tagName)) {
return; //避免多次引入报错
}
class AutoInputSelect extends HTMLElement {
/**
* 内部元素的dom,相当于document
*/
shadowRoot = null;
/**
* 构造参数
*/
constructor() {
super();
this.shadowRoot = this.attachShadow({mode: 'closed'});//元素内部的html不可见,为open时可见
this.shadowRoot.innerHTML = `<style> :host { --item-height: 25px; } .container { display: inline-block; position: relative; width: 200px; height: var(--item-height); font-size: 13px; background-color: #a6e22e; } .container input { width: 100%; height: 100%; box-sizing: border-box; font-size: 13px; padding: 0 5px; outline: none; border-radius: 2px; border: 1px solid #DADADA; } .container input:focus { border: 1px solid #149bdf } .container>span{ display: none; position: absolute; cursor: pointer; width: 15px; height: 15px; top: calc(50% - 8px); color: #b6b6b6; right: 5px; border-radius: 7px; } .container:hover >span{ display: inline-block;background-color: white; } .container ul { display: none; position: absolute; top: 100%; left: 0; width: 100%; box-sizing: border-box; max-height: 800%; overflow-y: auto; border: 1px solid #ccc; border-radius: 4px; background-color: #fff; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); z-index: 1000; padding: 0; margin: 0; } .container ul li { list-style: none; line-height: var(--item-height); height: var(--item-height); cursor: pointer; padding: 0 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .container ul li:hover { background-color: #f0f0f0; } ::-webkit-scrollbar { height: 10px; width: 6px; } ::-webkit-scrollbar-thumb { background: #7f7f7f80; background-clip: padding-box; border: 1px solid transparent; border-radius: 10px; } </style> <div class="container"> <input type="text" autocomplete='off'> <span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="m466.752 512-90.496-90.496a32 32 0 0 1 45.248-45.248L512 466.752l90.496-90.496a32 32 0 1 1 45.248 45.248L557.248 512l90.496 90.496a32 32 0 1 1-45.248 45.248L512 557.248l-90.496 90.496a32 32 0 0 1-45.248-45.248z"></path><path fill="currentColor" d="M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"></path></svg></span> <ul></ul> </div>`;
}
/**
* 对外提供的标签属性
*/
type = 'code';//功能类型,目前有码值下拉(code默认)和文本匹配(text)两种
data = null;//下拉选数据源,数组字符串,或者数组对象 ,必须包含name和value
value = null;//双向绑定的值
style = null;//外壳样式
placeholder = '--请选择--';//提示文本
inputStyle = null;//设置输入框的样式
itemStyle = null;//这个是直接追加在元素上的属性样式,将会直接追加在选项元素上
selectMaxHeight = null;//下拉选的最大高度 可以是百分比,会参照输入框的高度展示
loadMaxNum = 200;//选项数量加载限制,数量多了导致页面卡顿
/**
* 定制化
* 可以定制一些参数,用于集成到系统中,同时修改下方初始化逻辑
*/
/**
* 注册并监控标签属性
* 在这里定义,可以被监控数据变化,实时更新元素内容
* 不在这里定义 也可以主动通过 this.getAttribute('style')获取指定属性值
* 区别就是一个被动接收可以实时更新,一个主动获取
*/
static get observedAttributes() {
return ['type', 'value', 'data', 'style', 'placeholder', 'input-style', 'item-style', 'select-max-height', 'load-max-num'];
}
/**
* 这里用于处理监控到的标签属性变化
* @param name 属性名称
* @param oldValue 属性旧值
* @param newValue 属性新值
*/
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case 'type':
this.type = newValue;
break;
case 'value':
this.setValue(newValue);
break;
case 'style':
this.style = newValue;
if (this.isFinish) this.div.style.cssText = this.style;
break;
case 'data':
this.setData(newValue);
break;
case 'placeholder':
this.placeholder = newValue;
if (this.isFinish) this.input.placeholder = newValue;
break;
case 'input-style':
this.inputStyle = newValue;
if (this.isFinish) this.input.style.cssText = this.inputStyle;
break;
case 'item-style':
this.itemStyle = newValue;
this.createItem(this.input.value);
break;
case 'select-max-height':
this.selectMaxHeight = newValue;
if (this.isFinish) this.ui.style.maxHeight = this.selectMaxHeight;
break;
case 'load-max-num':
this.loadMaxNum = newValue;
break;
}
}
/**
* 元素对象
*/
div = null;//外壳
input = null;//输入框
ui = null;//下拉选框
/**
* 着重解释:这是一个隐藏的input元素,用于代替本元素表单提交,当存在name属性时触发创建
* 因为没有找到解决本元素参数绑定到表单的方法,只能创建一个隐藏的input插在页面上使用
* 实时同步value到这个input中,代替本元素表单提交
*/
inputElement = null;
/**
* 选项内容样式回调,提供用户自定义
* 回调入参(全量数据data,单个数据item,查询字符串keyWord)
*/
itemStyleCallback = null;//这个是设置选项展示内容的样式,不能控制选项元素本身
/**
* 事件标记
*/
isCreateItem = false;//是否正在创建下拉选项,避免多次调用冲突
isFinish = false;//本元素的html是否渲染完成
isFocus = false;//输入框是否聚焦
/**
* 当前选中数据项(手动指定值的时候,选项不存在时,会创建一个临时选项,解决码值越界也能正常读取写入value的问题)
*/
selectedItemData = null;
/**
* html渲染完成回调,做一些事件初始化,数据初始化操作
*/
connectedCallback() {
// this.shadowRoot 这个是用来获取,本元素内部的html元素,与外部document隔离的
this.div = this.shadowRoot.querySelector('div');
this.input = this.div.children[0];
this.ui = this.div.children[2];
this.div.children[1].addEventListener('click', (event)=>{
event.stopPropagation();
this.setValue(null)
this.createItem();
});
//根据name判断是否表单绑定
if (this.getAttribute("name")) {
this.inputElement = document.createElement('input');
this.inputElement.name = this.getAttribute("name");
this.inputElement.type = 'hidden';
this.parentElement.appendChild(this.inputElement);
}
this.isFinish = true;//这个主要是为了标签属性值监控部分加的标识
//追加样式
if (this.style && this.style !== '') this.div.style.cssText = this.style;
this.input.style.cssText = this.inputStyle;
this.input.placeholder = this.placeholder;
if (this.selectMaxHeight) this.ui.style.maxHeight = this.selectMaxHeight;
//定制化逻辑,根据定制化属性初始化数据项,可在这写
//初始化数据完成后,将初始化值绑定到本元素上,如输入框默认展示对应的选项
this.setValue(this.value);
//选项点击事件
this.ui.addEventListener('click', (event) => {
//有时候可能点中li内部的元素,这里循环查找
let li = event.target;
while (li.parentElement && li.tagName !== 'LI') {
li = li.parentElement;
}
if (li.tagName === 'LI') {
this.setValue(li.data, true);
}
event.stopPropagation();
this.close();//点击选项后手动关闭下拉选
});
//将输入框的这些事件绑定到本元素上,用于给开发者使用
this.input.addEventListener('input', this._handleInput);
this.input.addEventListener('focus', this._handleFocus);
this.input.addEventListener('blur', this._handleBlur);
}
//input与自定义元素事件绑定,转发
_handleInput = (event) => {
this.createItem(this.input.value);
try {
this.dispatchEvent(event);
} catch (e) {
}
}
_handleFocus = (event) => {
this.isFocus = true;
this.open();
this.createItem(this.input.value);
try {
this.dispatchEvent(event);
} catch (e) {
}
}
_handleBlur = (event) => {
this.isFocus = false;
try {
this.dispatchEvent(event);
} catch (e) {
}
}
/**
* 自定义下拉选触发事件
*/
_handleChange = (data) => {
this.dispatchEvent(new CustomEvent('select', {detail: data}));
}
_handleOpen() {
this.dispatchEvent(new CustomEvent('open'));
}
_handleClose() {
this.dispatchEvent(new CloseEvent('close'));
}
/**
* 根据搜索词创建匹配的下拉选项
*/
createItem(keyWord) {
if (!this.isFinish || this.isCreateItem) {
// console.log("取消操作:初始化未完成,或者多次同时创建");
return;
}
this.isCreateItem = true;
if (this.data == null) {
this.data = [];
}
this.ui.innerHTML = "";
this.ui.style.opacity=1;
let count = 0;
for (let item of this.data) {
if (this.loadMaxNum <= count) break;
if (!keyWord || this.getItemName(item).indexOf(keyWord) > -1) {
let showHtml = this.getItemHtml(this.data, item, keyWord);
const newElement = document.createElement('li');
newElement.data = item;
newElement.innerHTML = showHtml;
newElement.style.cssText = this.itemStyle;
this.ui.appendChild(newElement);
count++;
}
}
if(this.ui.innerHTML===''){
this.ui.style.opacity=0;
}
this.isCreateItem = false;
}
//创建选项展示内容的html代码
getItemHtml(data, item, keyWord) {
if (this.itemStyleCallback) {
return this.itemStyleCallback(data, item, keyWord)
} else if (this.type === 'code') {
return this.getString(item.name);
} else if (this.type === 'text') {
return this.getString(item);
}
}
//转字符串
getString(value) {
if (!value) return '';
// 判断值的类型
if (typeof value === 'object') {
// 如果是对象类型,转换为 JSON 字符串
return JSON.stringify(value);
} else {
// 否则,直接调用 toString 方法转换为字符串
return String(value);
}
}
/**
* 监听下拉选打开后的点击操作
* (1)点击了下拉选选项,这里就不处理了,这里因为ul比document先一步拿到点击事件,且选中选项后会关闭下拉选,因此通过判断下拉选已经关闭,来判断点击了选项
* (2)点击输入框里面,不做处理,因为输入框失焦比ul拿到点击还要早,因此这里通过判断输入框聚焦状态,来判断点击了输入框
* (3)点击其他地方,关闭下拉选
*/
_handleClick = (event) => {
if (!this.isFocus && this.isOpen()) {
if (this.input.value === '') {
this.setValue(null)
} else {
this.input.value = this.getItemName();
}
this.close();
}
}
//手动打开下拉选
open() {
this.ui.style.display = 'block';
document.addEventListener('click', this._handleClick);
this._handleOpen();
}
//手动关闭下拉选
close() {
this.ui.style.display = 'none';
document.removeEventListener('click', this._handleClick);
this._handleClose();
}
isOpen() {
return this.ui.style.display === 'block';
}
/**
* 设置下拉选数据源
* 格式[{name:'xxx',value:'xxx'}]
* @param data 数据源
* @param valueCallback 数据中没有value,需要自定义映射
* @param nameCallback 数据中没有name,需要自定义映射
*/
setData(data, valueCallback, nameCallback) {
try {
if (typeof data === 'string') {
data = JSON.parse(data);
}
if (!(data instanceof Array)) {
console.warn("数据不合法,请提供数组数据,格式:[{name:'xxx',value:'xxx'},...,{name:'xxx',value:'xxx'}]");
}
if (nameCallback || valueCallback) {//有自定义映射
for (const item of data) {
if(valueCallback) item.value=valueCallback(item)
if(nameCallback) item.name=nameCallback(item)
if (this.value && this.value === item.value) this.setValue(item);
this.data.push(item);
}
} else {//没有自定义映射
this.data = data;
if (this.value) this.setValue(this.value);
}
this.createItem();
} catch (e) {
console.warn("数据解析报错", data);
throw new Error(e);
}
}
//根据值进行数据搜索
findData(value) {
if (this.data == null) {
return null;
}
for (let item of this.data) {
if (value === item.value) return item;
}
return null;
}
//手动搜索
searchName(keyWord) {
this.createItem(keyWord);
if (this.isFinish) this.input.value = keyWord;
}
getKeyWord(){
return this.input.value;
}
//设置自定义选项样式回调
setItemStyle(callback) {
this.itemStyleCallback = callback;
}
//获取当前选中值的文本,换句话说是输入框的文本
getItemName(item) {
let itemTmp = this.selectedItemData;
if (item) itemTmp = item;
if (!itemTmp) return null;
if (this.type === 'code') {
return itemTmp.name || '';
} else if (this.type === 'text') {
return this.getString(itemTmp);
}
}
//获取当前选中值
getValue() {
return this.value;
}
/**
* 允许手动设置选中值
* @param value 要设置的选中值
* @param isItemData 表示传递的value是含value的数据项,无需去data中查询,数据量大的时候相当与小优化
*/
setValue(value, isItemData) {
if (this.type === 'code') {
if (isItemData) {
this.selectedItemData = value;
} else {
let item = this.findData(value);
if (item) {
this.selectedItemData = item;
} else {//空值,未匹配的值
this.selectedItemData = {name: '', value: value}
}
}
this.value = this.selectedItemData.value;
} else if (this.type === 'text') {
this.value = value;
this.selectedItemData = value;
}
this._handleChange(this.selectedItemData);
if (this.input) this.input.value = this.getItemName();
if (this.inputElement) this.inputElement.value = this.getString(this.value);
}
// 获取自定义元素的值
get value() {
return this.getValue();
}
// 设置自定义元素的值
/**
* @param {any} val
*/
set value(val) {
this.setValue(val);
}
}
// 注册自定义元素到html中
customElements.define(tagName, AutoInputSelect);
})();
高级使用
我在代码中留下了定制化关键字,当你会写自定义标签的代码时,可以根据位置,追加自定义代码,以便于集成到系统中去,如传递码值类型,在初识化的代码中读取码值类型,异步请求获取系统码值数据进行初始化,这样在系统中使用时,就无需写很多的js代码控制,很方便的实现自动补全下拉选。
文本模糊搜索功能也是一样道理。
注意是刚写的,未经充分验证,有问题欢迎指正