demo :
https://codepen.io/bangking007/pen/qGNxvv
核心代码:
comboGrid.js
Vue.component("comboxGrid", {
props: {
// input的数据源(v-model)
value: {
type: String,
default: ''
},
// 下拉表格的数据源(由JObject组成的数组)
rowSource: {
type: Array,
required: true
},
// 当搜索成功或选取行数据后,将要获取其中的字段值写入【fylSource】的字段名,如果没有指定,将默认使用第一列
returnFld: {
type: String
},
// 下拉表格各列的定义,如[{field:"code",title:"编号",width:100},...]
columns: {
type: Array,
required: true
},
// 输入框是否显示清除按钮,默认为true
clearable: {
type: Boolean,
default: true
},
// 输入框是否只读,默认 false
readonly: {
type: Boolean,
default: false
},
// 是否框是否为Disabled,默认 false
inputDisabled: {
type: Boolean,
default: false
},
// 是否得到焦点时才允许显示下拉按钮和清除按钮(有值并且不是只读)
// 默认为否,即不管是否得到焦点,都允许显示
isSparse: {
type: Boolean,
default: false
},
// 更新数据前的回调函数,如果返回false,将不更新
beforeUpdate: {
type: Function
}
},
data() {
return {
// 本组件中的input元素 绑定的数据源v-model,初始值是本组件传入的v-model
controlSource: "test",
// 内部使用,如果mounted后,会自动为true
isMounted: false,
//内部使用,指示本组件中的el-popover是否显示
isShow: false,
//本组件的input元素得到焦点时为true,否则为false
isInputFocus: false,
//el-popover得到焦点时为true,否则为false
isPopFocus: false,
// 是否过滤,当点击下拉按钮或按F4,将显示全部,并且定位到相关的行
isFilter: true,
// 下拉表格最后选中的当前行数据
currentRow: {
recno: -1,
data: null
},
// 内部使用,当点击了清除按钮,会触发为true,从而改变了controlSource,controlSource的监控器会自动把它reset为false
clearing: false,
// 内部请求hide的时候,会触发为true,从而改变了controlSource,controlSource的监控器会自动把它reset为false
// 其实用watch也可以实现,目的是减少watch的滥用
hiding: false,
// 内部使用,当本组件的vmodel数据源发生变化是,此值为true
vmodel_changing: false
}
},
mounted() {
const that = this;
that.$nextTick(function() {
// 初始input元素的值
that.controlSource = that.value;
// 禁止下拉按钮具有焦点
let $btnContainer = $(that.$el).find("i").parent();
if ($btnContainer.length > 0) {
$btnContainer[0].removeAttribute("tabindex");
}
});
},
computed: {
// 当input或pop得点焦点时为true,否则为false(如果为false,强制隐藏pop)
isFocus() {
const that = this;
let result = that.isInputFocus || that.isPopFocus;
return result;
},
// 清除按钮的动态显示实现
showClear() {
const that = this;
return that.clearable &&
!that.inputDisabled &&
!that.readonly &&
(!that.isSparse || that.isSparse && that.isFocus) &&
((that.controlSource + "").length > 0);
},
// 下拉钮按的动态显示实现(如果readonly和disabled都为flase时,返回true)
showDropDown() {
const that = this;
return !that.inputDisabled &&
!that.readonly &&
(!that.isSparse || that.isSparse && that.isFocus)
},
// 实时过滤结果,则根据input元素的内容,在数组rowSource模糊过滤返回数组
searchResult: function() {
const that = this;
// 如果没有dataGrid显示,不搜索
if (!that.isShow) {
return [];
}
let result = that.rowSource,
expr = that.controlSource.trim(),
recno = -1;
if (expr) {
if (this.isFilter) {
// 模糊过滤结果
const reg = new RegExp(expr, "i");
result = that.rowSource.filter(item => {
return that.columns.some(column => {
return reg.test(String(item[column.field]))
})
});
}
}
return result;
}
},
watch: {
controlSource: function(curVal, oldVal) {
const that = this;
if (!that.isMounted) {
that.isMounted = true;
} else {
if (!that.isShow && that.isFocus && that.showDropDown) {
// 如果是因为点击清除按钮的就不触发
if (that.clearing || that.hiding || that.vmodel_changing) {
that.clearing = that.hiding = that.vmodel_changing = false;
} else {
that.show();
}
}
// 如果pop已经显示,搜索完全匹配的一行,如果没有找到,就取模糊过滤结果的第一行作为当前行
if (that.isShow) {
let recno = that.seek();
that.setCurrentRow(recno);
}
}
if (!that.isFilter) {
that.isFilter = true;
}
},
value: function(curVal, oldVal) {
if (curVal !== this.controlSource) {
this.vmodel_changing = true;
this.controlSource = curVal;
}
},
// 当本组件失去焦点时,验证input元素的值是否是rowSource中的,并且强制隐藏pop
isFocus: function(curVal, oldVal) {
if (!curVal && oldVal) {
const that = this;
//失去焦点,更新绑定的数据源
that.valid();
that.hide();
}
}
},
methods: {
// 显示或隐藏pop的方法,并且强制使input元素得到焦点
toggle(e) {
this.reset();
this.isShow = !this.isShow
this.focus();
if (this.isShow) {
let recno = this.seek();
this.setCurrentRow(recno);
this.isFilter = false;
}
},
// 隐藏pop
hide() {
if (this.isShow) {
this.reset();
this.isShow = false;
}
},
//显示pop
show() {
if (!this.isShow && this.showDropDown) {
this.reset();
this.isShow = true;
}
},
// 使弹出框每次都引用本控件
reset() {
let pop = this.$refs.elpopover;
pop.referenceElm = this.$el;
},
// 后备用
refresh(e) {
const that = this;
},
//input元素按下回车发生的事件
enter(e) {
// 回车时,如果弹窗已打开,默认使用选定的
const that = this;
if (that.isShow) {
//阻止默认行为
e.preventDefault();
//阻止冒泡
e.stopPropagation();
let fld = that.returnFld || that.columns[0].field;
if (fld in that.currentRow.data && that.currentRow.data[fld] !== that.controlSource) {
that.hiding = true;
that.controlSource = that.currentRow.data[fld];
}
//that.valid();
that.hide();
}
},
// 下拉按钮按了空格键或input按了空格键触发(不知道为什么keydown.space.stop阻止不了)
onSpace(e) {
const that = this;
let stop = true,
toggle = true;
if (e.target.nodeName.toLowerCase() === "input") {
// input按了空格键
if (that.controlSource.trim()) {
stop = false;
}
toggle = false;
}
if (stop) {
//阻止默认行为
e.preventDefault();
//阻止冒泡
e.stopPropagation();
if (toggle) {
that.toggle()
}
}
},
// 设置pop中的grid的高亮行(即选定的行)
// tRecno:将要高亮的行
// tDirection:滚动方向
setCurrentRow(tRecno, tDirection) {
const that = this,
result = that.searchResult;
tRecno = (tRecno >= 0 && tRecno < result.length) ? tRecno : -1;
let oldCurrentRow = that.currentRow;
that.currentRow = {
recno: tRecno,
data: (tRecno >= 0) ? result[tRecno] : null
};
that.$refs.dataGrid.setCurrentRow(that.currentRow.data);
// 如果过滤的集合行数大于1,有可能将要定位的那行未能显示,故要滚动到这行
if (result.length > 1 && tRecno >= 0 && typeof(tDirection) === "number") {
const tableWrapper = that.$refs.dataGrid.$el.getElementsByClassName("el-table__body-wrapper")[0];
const table = tableWrapper.getElementsByTagName(
"table")[0];
const direction = (tDirection < 0);
let scrollIntoView = () => {
// 下滚false,上滚 true
if (!that.isElementInViewport(tableWrapper, table.rows[tRecno])) {
table.rows[tRecno].scrollIntoView(direction);
}
};
if (table.rows.length <= 0) {
let tryCount = 10;
let interval = setInterval(() => {
if (table.rows.length > 0) {
if (tRecno < table.rows.length) {
scrollIntoView();
tryCount = -1;
}
}
tryCount--;
if (tryCount < 0) {
clearInterval(interval);
}
}, 100);
} else {
scrollIntoView();
}
}
//trList[that.currentRow.recno].scrollIntoView(true);
//console.info(that.$refs.dataGrid.$el.getElementsByClassName("el-table__body-wrapper"))
//console.info(that.$refs.dataGrid.$el.getElementsByTagName())
//anchor.scrollIntoView(true);
},
// 获取本组件的input元素
getInput() {
return this.$el.getElementsByTagName("input")[0];
},
// 使本组件的input元素得到焦点的方法
focus() {
let input = this.getInput();
input.focus()
input.select();
},
// 清除按钮触发的方法
clear(e) {
this.clearing = true;
this.controlSource = "";
this.$emit('clear');
this.valid();
this.focus();
},
// 模糊搜到的字符串改变颜色
formatOutput(tFldValue) {
if (tFldValue.indexOf(this.controlSource) !== -1 && this.controlSource !== '') {
//return tFldValue.replace(this.controlSource, '<font color="white" style="background-color:#FF9800;">' + this.controlSource + '</font>')
return tFldValue.replace(this.controlSource, '<font color="#409EFF">' + this.controlSource + '</font>')
} else {
return tFldValue
}
},
// 用户按下 up或down时的事件处理
move(e, tDirection) {
const that = this;
//阻止默认行为
e.preventDefault();
//阻止冒泡
e.stopPropagation();
if (that.isShow) {
const recno = that.currentRow.recno + tDirection;
if (recno >= 0 && recno < that.searchResult.length) {
that.setCurrentRow(recno, tDirection);
}
}
},
// 在当前过滤结果中查找首个与输入框匹配的行,作为默认高亮显示
seek() {
const that = this;
let recno = -1;
that.searchResult.some((item, index) => {
let found = that.columns.some(column => {
return item[column.field] == that.controlSource;
})
if (found) {
recno = index;
}
return found;
});
if (recno < 0 && that.searchResult.length > 0) {
recno = 0;
}
return recno;
},
// 更新本组件绑定的数据源
update(tNewValue) {
const that = this;
if (that.controlSource != tNewValue) {
that.controlSource = tNewValue;
}
/*
if (that.value != tNewValue) {
that.value = tNewValue;
}
*/
that.$emit('input', tNewValue)
},
// 离开验证并准备,如有变化,执行update更新本组件绑定的数据源
valid() {
const that = this;
let isPass = true;
if (that.isShow) {
that.hide();
}
if (that.currentRow && that.currentRow.data && that.controlSource) {
let fld = that.returnFld || that.columns[0].field;
if (fld in that.currentRow.data && that.currentRow.data[fld] !== that.value) {
if (that.beforeUpdate) {
isPass = that.beforeUpdate(that.currentRow.data);
if (isPass === false) {
that.controlSource = that.value;
}
}
if (isPass || isPass === undefined) {
that.update(that.currentRow.data[fld]);
}
}
} else {
if (!that.controlSource && that.value) {
if (that.beforeUpdate) {
isPass = that.beforeUpdate(that.currentRow.data);
if (isPass === false) {
that.controlSource = that.value;
}
}
if (isPass || isPass === undefined) {
that.update("");
}
}
}
},
// 双击pop里grid的行触发的事件,则把该行作为数据返回
dblclick(row, column, event) {
const that = this;
that.currentRow.recno = null;
that.currentRow.data = row;
that.hiding=true;
let fld = that.returnFld || that.columns[0].field;
if (fld in that.currentRow.data && that.currentRow.data[fld] !== that.controlSource) {
that.focus();
that.controlSource = that.currentRow.data[fld];
}
that.valid();
},
//判断元素tEl的各边界是否完全在父级容器tContainer所在的区域内,如果是,返回true,否则返回false
//主要是datagrid的当前行,如果不是在表格内显示,将要滚动它
isElementInViewport(tContainer, tEl) {
var elRect = tEl.getBoundingClientRect(),
parRect = tContainer.getBoundingClientRect();
return (
elRect.top >= parRect.top &&
elRect.left >= parRect.left &&
elRect.bottom <= parRect.bottom &&
elRect.right <= parRect.right
);
}
},
template: `
<div class="combox"
v-bind:class="[isFocus ? 'combox-focus' : '']"
>
<div @focusin="isInputFocus=true" @focusout="isInputFocus=false"
class="input-with-select el-input el-input-group el-input-group--append el-input--suffix">
<input v-bind:readonly="readonly" type="text" autocomplete="off" placeholder="请输入"
v-model="controlSource"
@keydown.esc="hide()"
@keydown.f4="toggle()"
@keydown.up.stop="move($event,-1)"
@keydown.down.stop="move($event,1)"
@keydown.enter="enter"
@keydown.space="onSpace"
class="el-input__inner">
<span v-if="showClear" class="el-input__suffix" style="transform:translateX(-16px);" @click="clear">
<span class="el-input__suffix-inner">
<i style="padding:0 8px;" class="el-input__icon el-icon-circle-close el-input__clear"></i>
</span>
</span>
<div v-if="showDropDown" class="el-input-group__append waves-effect" @keydown.space="onSpace" @click="toggle">
<el-popover ref="elpopover"
placement='bottom-start'
v-model="isShow"
trigger="manual" >
<div tabindex="0" style="outline:none;"
@keydown.up.stop="move($event,-1)"
@keydown.down.stop="move($event,1)"
@focusin="isPopFocus=true" @focusout="isPopFocus=false">
<!--
<el-button class="waves-effect" icon="el-icon-refresh" style="float: right; " @click="refresh"></el-button>
-->
<slot></slot>
<el-table ref="dataGrid" max-height="500" :hieght="400" @row-dblclick="dblclick" :data="searchResult" highlight-current-row>
<el-table-column v-for="(column,colno) in columns"
:key="column.field"
:prop="column.field"
:label="column.title"
:width="column.width">
<template slot-scope="scope">
<span :id="'test'+scope.$index+'_'+colno" v-html="formatOutput(scope.row[column.field])" ></span>
</template>
</el-table-column>
</el-table>
</div>
<div slot="reference">
<transition name="el-zoom-in-top">
<i style="font-size: 16px;padding: 0 8px;" v-bind:class="[isShow ? 'el-icon-arrow-up' : 'el-icon-arrow-down']"></i>
</transition>
</div>
</el-popover>
</div>
</div>
</div>
`
});
使用方法:
<div>
<combox-grid style="left:50px;width: 200px;" v-model="state" :before-update="check" :columns="subjects_columns"
:row-source="test1">
</combox-grid>
</div>
<div>
<!--使用v-slot,在表格前插入按钮-->
<combox-grid style="left:50px;width: 200px;" v-model="state" :columns="columns" :is-sparse="true" :row-source="test2">
<template v-slot="">
<el-button class="waves-effect" icon="el-icon-refresh" style="float: right; "></el-button>
<button>test</button>
</template>
</combox-grid>
</div>