目录
例如:两个对象 :value1: { name: "字段1", value: 1 }, value2: { name: "字段2", value: 2 }
展示 "字段1"+"字段2",为(1+2) 运算"+"符号也是可以输入替换的
1.实现起来并不复杂这个vue中使用一个循环即可实现上图效果
<span v-for="item in list" :key="item.id">
{{ item.name }}
</span>
2.输入框肯定是有是光标, 它是 一闪一闪的(显示隐藏)有规律的,这里就不必使用定时器了简单的动画css就可以实现:
.curcus-text {
width: 1px;
background-color: #0a89f3;
display: block;
animation: text 1s infinite;
position: absolute;
left: 7px;
top: 8px;
}
@keyframes text {
from {
width: 1px;
}
to{
width: 0;
}
}
3.字段展示和光标都有了, 点击字段改变index位置, 用index改变光标位置
/*methods*/
setInterval(index) {
this.intervalIndex = index;
},
4.时候已经可以通过index更改指定数据了, 接下来就是一些dom的操作了,当index发生改变时对应光标位置也要发生改变, 将$refs.curcusText的left, 和top属性更改问点击的<span></span>的offsetLeft+offsetWidth, 和offssetTop 简单即可
<span v-for="(item, index) in list" :key="item.id" @click="setInterval(index)">
{{ item.name }}
</span>
<span v-show="isCurcusText" class="curcus-text" ref="curcusText"></span>
5.或者是每个字段间隔都有一个光标currentIndex所在哪里则展示那个光标同样可以实现
<span v-for="(item, index) in list" :key="item.id" @click="setInterval(index)">
{{ item.name }}
<span v-show="index === currentIndex" class="curcus-text" ref="curcusText"></span>
</span>
6.我们还得考虑正常空字段数组为空的话$refs.curcusText的所在位置, 可以$refs.fromInterval来确定,正常情况下我们点击第二个字段光标是显示在第一个字段后面的, 这里用offsetLeft来确定位置还是offsetLeft+offsetWidth加上setInterval(index-1)都是可以是实现的
<span ref="fromInterval"></span>
<span v-for="(item, index) in list" :key="item.id" @click="setInterval(index-1)">
{{ item.name }}
</span>
<span v-show="isCurcusText" class="curcus-text" ref="curcusText"></span>
/*watch*/
//光标位置
intervalIndex: {
handler(index) {
let { valueItems, fromInterval }= this;
let x, y;
if (~index && valueItems?.length && valueItems[index]?.offsetLeft) {
let valueItem = valueItems[index];
x = valueItem.offsetLeft + valueItem.offsetWidth;
y = valueItem.offsetTop;
} else {
x = fromInterval?.offsetLeft;
y = fromInterval?.offsetTop;
}
this.setCurcusTextIndex(x, y);
this.$emit('update:index', index);
},
deep: true,
immediate: true
},
/*methods*/
//curcusText位置
setCurcusTextIndex(x, y) {
this.$nextTick(() => {
if (this.$refs.curcusText) {
this.$refs.curcusText.style.left = x +"px";
this.$refs.curcusText.style.top = (y+1) +"px";
}
})
},
7.对于获取焦点和失去焦点这里我使用了监听document的点击事件来实现失去焦点, 和this.$el点击showCurcusText()获取焦点, 写在获取焦点document的点击中也是可以的,基本就这样就好了,具体代码看,组件textarea的template即可
//显示光标
showCurcusText(event) {
let { valueItems } = this;
let excludeDomList = valueItems;
if (!~excludeDomList.findIndex(item => item === event.target)) {
this.isCurcusText = true;
let index = ~~valueItems?.length-1;
this.intervalIndex = index;
}
},
//document监听所有点击事件消除isCurcusText
documentClikc() {
document.addEventListener('click', (e) => {
let excludeDomList = [].concat( this.valueItems, this.exclude);
if (e.target !== this.$el && !~excludeDomList.findIndex(item => item === e.target)) {
this.isCurcusText = false;
}
})
},
一、组件textarea的template
示例:该组件是从textarea标签角度去封装的, 使用案例在页面page的template页面中
<template>
<div class="textarea-box border-gray" @click="showCurcusText">
<span ref="fromInterval"></span>
<span v-for="(item, index) in valueArr" style="display:flex;" :key="index" ref="valueItem"
:style="{ paddingLeft: paddingLeft ? paddingLeft[0]+'px' : 0,
paddingRight: paddingLeft ? paddingLeft[1]+'px' : 0 }"
@click="setInterval(index-1)">
{{ item.name }}
</span>
<span v-show="isCurcusText" class="curcus-text" ref="curcusText"></span>
</div>
</template>
<script>
export default {
props: {
valueList: Array, //数组型(值)
isShow: Boolean, //光标 (代表焦点)
index: Number, //光标所在索引
exclude: { //点击dom不失去光标 dom数组
type: Array,
default: () => []
},
paddingLeft: { //值的左右padding[left, right]
type: Array,
default: () => [5, 1]
}
},
data() {
return {
isCurcusText: false, //光标
intervalIndex: -1, //光标索引 (-1~length)
fromInterval: null, //光标起始位置dom
valueArr: [],
arrStore: [],
valueItems: [],
};
},
watch: {
valueList: {
handler(val) {
this.valueArr = val;
},
deep: true,
immediate: true
},
valueArr: {
handler(val) {
this.$emit('update:valueList', val);
this.getExcludeDom(() => {
let len = val.length - this.arrStore.length;
this.arrStore = JSON.parse(JSON.stringify(val));
if (this.intervalIndex < val.length) {
if (this.valueItems?.length) {
this.intervalIndex += len > 0 ? len : 0;
}
}
});
},
deep: true,
immediate: true
},
isShow(val) {
this.isCurcusText = val;
},
isCurcusText(val) {
this.$emit('update:isShow', val);
},
//光标位置
intervalIndex: {
handler(index) {
let { valueItems, fromInterval }= this;
let x, y;
if (~index && valueItems?.length && valueItems[index]?.offsetLeft) {
let valueItem = valueItems[index];
x = valueItem.offsetLeft + valueItem.offsetWidth;
y = valueItem.offsetTop;
} else {
x = fromInterval?.offsetLeft;
y = fromInterval?.offsetTop;
}
this.setCurcusTextIndex(x, y);
this.$emit('update:index', index);
},
deep: true,
immediate: true
},
index(i) {
this.intervalIndex = i;
}
},
methods: {
//curcusText位置
setCurcusTextIndex(x, y) {
this.$nextTick(() => {
if (this.$refs.curcusText) {
this.$refs.curcusText.style.left = x +"px";
this.$refs.curcusText.style.top = (y+1) +"px";
}
})
},
//点击间隙
setInterval(index) {
this.isCurcusText = true;
this.intervalIndex = index;
},
//显示光标
showCurcusText(event) {
let { valueItems } = this;
if (!~valueItems.findIndex(item => item === event.target)) {
this.isCurcusText = true;
let index = ~~valueItems?.length-1;
this.intervalIndex = index;
console.log(valueItems)
}
},
//删除valueItem
deleteIntervalBtn() {
if (~this.intervalIndex) {
this.valueArr.splice(this.intervalIndex, 1);
this.intervalIndex--;
}
},
//光标左右移动
setIntervalIndex(state) {
if (state) {
let index = this.intervalIndex+1;
let maxIndex = this.valueArr.length-1;
this.intervalIndex = index > maxIndex ? maxIndex : index;
}else {
let index = this.intervalIndex-1;
let minIndex = -1;
this.intervalIndex = index < minIndex ? minIndex : index;
}
},
//获取不包含dom
getExcludeDom(fuc) {
this.$nextTick(() => {
this.valueItems = this.$refs.valueItem;
fuc();
})
},
//document监听所有点击事件消除isCurcusText
documentClikc() {
document.addEventListener('click', (e) => {
let excludeDomList = [].concat( this.valueItems, this.exclude);
if (e.target !== this.$el && !~excludeDomList.findIndex(item => item === e.target)) {
this.isCurcusText = false;
}
})
},
//document监听删除和左右键
documentKey() {
document.addEventListener('keydown', (e) => {
let k= e.keyCode;
if (!this.isCurcusText || !this.valueArr.length) return null;
switch (k) {
case 8: //backspace删除
this.deleteIntervalBtn();
break;
case 46: //delete删除
this.deleteIntervalBtn();
break;
case 37: //左
this.setIntervalIndex(0);
break;
case 39: //右
this.setIntervalIndex(1);
break;
}
})
},
//获取光标起始位置
getFromIntervalDom() {
this.fromInterval = this.$refs.fromInterval;
}
},
mounted() {
this.$nextTick(() => {
this.getFromIntervalDom();
this.documentClikc();
this.documentKey();
})
}
}
</script>
<style scoped>
.textarea-box {
width: 100% !important;
height: 150px;
color: #707070;
padding: 7px;
}
.border-gray {
border: 1px solid rgb(225, 225, 225);
overflow: hidden scroll;
cursor: text;
position: relative;
}
.border-gray::-webkit-scrollbar {
display: none;
}
.border-gray span {
height: 20px;
display: block;
float: left;
}
.interval-btn {
width: 5px;
}
.curcus-text {
width: 1px;
background-color: #0a89f3;
display: block;
animation: text 1s infinite;
position: absolute;
left: 7px;
top: 8px;
}
@keyframes text {
from {
width: 1px;
}
to{
width: 0;
}
}
</style>
二、页面page的template
这里我 import pushTextarea from "@/components/common/input/pushTextarea"; 导入封装好的pushTextarea组件 复制是请注意自己的文件路径
<template>
<div class="content-box" ref="boxDom">
<div class="calculator-box">
<div class="calculator-input-warp">
<div class="form-item-box">
<span class="form-item-box-title">计算公式:</span>
<push-textarea ref="computationalPushTextarea"
v-model:is-show="isShow" v-model:value-list="valueList" v-model:index="index" :exclude="btnItemDomList"/>
</div>
<div class="btn-item" @click="consoleValue()" style="width: 100px;">
输出
</div>
</div>
<div class="calculator-btn-warp">
<div class="btn-warp-box">
<div class="btn-warp">
<div v-for="item in btnList" :key="item.id" class="btn-warp-item" ref="btnItem">
<div class="btn-item" ref="btnItem"
@click="pushList(item)">
{{ item.name }}
</div>
</div>
</div>
<div class="btn-warp" style="margin-top: 20px;">
<div v-for="item in paramsBtn" :key="item.id" class="btn-warp-item" ref="btnItem">
<div class="btn-item" ref="btnItem"
@click="pushList(item)">
{{ item.name }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import pushTextarea from "@/components/common/input/pushTextarea";
export default {
components: {
pushTextarea
},
data() {
return {
btnItemDomList: [],
index: -1,
isShow: false,
valueList: [{id: 1, name: "test"}],
btnList:[
{ id:'+' ,name: '+'}, { id:'-' ,name: '-'}, { id: '*', name: "*" }, { id: '/', name: '/' }, { id: '(', name: "(" }, { id: ')', name: ")" },
{ id:'=' ,name: '='}, { id:'>' ,name: '>'}, { id: '>=', name: ">=" }, { id: '<', name: '<' }, { id: '<=', name: "<=" }, { id: '<>', name: "<>" },
{ id:'并且' ,name: '并且'}, { id:'或者' ,name: '或者'}, { id: 'case', name: "case" }, { id: '如果', name: '如果' }, { id: '则', name: "则" },
{ id: '否则', name: "否则" }, { id: 'end', name: "end" }, { id: 46, name: "删除" },
],
paramsBtn: [
{ id: 1, name: "字段1", value: 7 },
{ id: 2, name: "字段2", value: 16 },
{ id: 3, name: "字段3", value: 8 },
]
};
},
methods: {
//获取字段
consoleValue() {
console.log(this.valueList);
this.valueList = [{id: 2, name: "testtesttest"}];
},
//添加运算字段
pushList(text) {
if (this.isShow){
if (text.id !== 46) {
this.valueList.splice(this.index+1, 0, text);
}else { //删除
this.$refs?.computationalPushTextarea && this.$refs.computationalPushTextarea?.deleteIntervalBtn();
}
}
},
//获取btn-item 元素列表
getBtnItemDomList() {
this.$nextTick(() => {
this.btnItemDomList = this.$refs.btnItem;
})
},
//清除pushTextarte缓存
clearTextarea() {
this.index = -1;
this.isShow = false;
this.valueList = [];
this.getBtnItemDomList();
}
},
mounted() {
this.getBtnItemDomList();
}
};
</script>
<style scoped>
.content-box {
margin-top: 50px;
height: 100%;
width: 100%;
padding: 0 20px;
overflow: hidden scroll;
}
.foot-box {
right: 0;
bottom: 0;
width: 100%;
border-top: 1px solid #e9e9e9;
padding: 10px 16px;
background: #fff;
text-align: right;
z-index: 1;
}
.flex-box {
display: flex;
}
.flex-box > div {
flex: 1;
}
.calculator-box {
display: flex;
}
.calculator-box div {
flex: 1;
}
.form-item-box {
display: flex;
margin-bottom: 20px;
}
.form-item-box-title {
margin-right: 10px;
width: 80px;
text-align: right;
}
.calculator-btn-warp {
height: auto;
}
.btn-warp-title {
color: red;
}
.btn-warp {
position: relative;
display: flex;
flex-wrap: wrap;
}
.btn-warp-item {
min-width: 20%;
padding: 5px;
position: relative;
box-sizing: content-box;
}
.btn-item {
display: flex;
justify-content: center;
align-items: center;
padding: 5px 10px;
color: #1295f2;
border-radius: 5px;
border: 1px solid #1295f2;
cursor: pointer;
}
</style>
1.显示且更改数据片段
代码如下(示例):
//添加运算字段 pushList(text) { if (this.isShow){ if (text !== 46) { this.valueList.splice(this.index+1, 0, text); }else { //删除 this.$refs?.computationalPushTextarea && this.$refs.computationalPushTextarea?.deleteIntervalBtn(); } } },
此函数调用的添加删除数组操作,组件双向绑定了index, isShow(两个来控制是状态和位置), valueList双向绑定的数组,操作他则页面对此更改。由于点击其他元素我们因该让isShow=false, document.addEventListener()监听document点击来操作“isShow=false”,按钮我们是不需要操作“isShow=false”的所以对此btnItemDomList这个参数就是传递点击不执行”isShow=false“的DOM列表。
2.使用组件注意案例
1.每次btnList更改后btnItem则已经改变对此exclude也需要发生改变则调用getBtnItemDomList()函数重新获取btnItem, 还有每次valueList重新赋值(不指更改数组内元素). 当valueList = [{id: 1, name: "test"}]时候点击this.$el光标位置显示最后一个字段后, 又改变为valueList = [{id: 2, name: "testtesttest"}],再次点击this.$el光标位置本因该显示最后一个字段后
可光标位置并没有改变这是因为我们改变位置是根据监听intervalIndex发生改变而重新获取光标位置, 而数据valueArr长度改变则intervalIndex发生改变,实则valueArr发生了变化但长度并没有改变只不过字段改变了,由于字段长度更长(短也一样)了字段后的位置因该是光标所位置, 但并不一致了,这是后我么们如果要改变valueArr则希望先调用clearTextarea()函数把数据重新初始化.在进行赋值则将问题解决了. 如果使用最前面说的每个字段后(或前)都加上光标对应展示则不会有些这问题, 且很多地方都很更加简单.
总结
提示:这里对文章进行总结: