效果图:
思路
原本是想弄一个输入框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和valuevalue = 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代码控制,很方便的实现自动补全下拉选。
文本模糊搜索功能也是一样道理。
注意是刚写的,未经充分验证,有问题欢迎指正