需求
自定义公式输入框,要求:
1、通过点击的方式添加数据和符号;
2、自定义输入框中可以像普通输入框那样,在光标位置插入输入;
3、用户通过delete和backspace按键可以删除;
4、点击输入框,光标定位到最后;
npm直接使用
//安装
npm i element-textarea
//引入
import ElementTextarea from 'element-textarea'
import 'element-textarea/lib/element-textarea.css'
未打包的文件地址:element-textarea-origin
实现思路
1、沿用原生的 —— 失败
最初的想法是通过改造原生的输入框, 可以直接沿用本身自带的光标,
找到一个例子参考: 写一个可插入自定义标签的 Textarea 组件
这个里面666可以看作一个小组件,那应该可以满足上述需求!
但想象很丰满,现实很骨干!原生的光标只针对文字有效!对于组件,也是针对组件的内部文字起效!
加个边框就出现下面这种效果:
光标在组件里面,完全不行的啊摔!
2、只要那个光标就好
其实删除,插入什么的都好实现,那现在就剩那个光标的效果了,比如我这么干:
在小组件两边加个input,把框框去掉,不就有光标了!
可行!
先创建一个EditMock.vue, 模拟组件两边的输入光标, 实现键盘delete: 46 和backspace: 8的删除
<template>
<div class="edit-mock">
<input
v-if="!noPre"
ref="preInput"
class="edit-mock__input"
@keyup.46="handleDelete('now')"
@keyup.8="handleDelete('pre')"
@click.stop="handleClick('pre')"
v-model="str"
@input="noStrInput"
/>
<slot></slot>
<input
v-if="!noSub"
ref="subInput"
class="edit-mock__input"
@keyup.46="handleDelete('sub')"
@keyup.8="handleDelete('now')"
@click.stop="handleClick('now')"
v-model="str"
@input="noStrInput"
/>
</div>
</template>
<script>
export default {
props: {
compId: {
type: String,
default: '',
},
// 前面的input不显示
noPre: {
type: Boolean,
default: false,
},
// 后面的input不显示
noSub: {
type: Boolean,
default: false,
},
},
data() {
return {
str: '',
}
},
methods: {
handleDelete(type) {
this.$emit('edit-keyup', { compId: this.compId, type })
},
handleClick(type) {
this.$emit('edit-click', { compId: this.compId, type })
},
// 除了光标,什么都不让输入
noStrInput() {
this.str = ''
},
},
}
</script>
<style lang="scss" scoped>
.edit-mock {
display: flex;
height: 100%;
&__input {
text-align: center;
width: 10px;
height: 100%;
background: #fff;
border: none;
}
}
</style>
这里noPre的作用主要是除了第一个组件需要在前面插入, 其他的都只需要后面的input即可。
3、 公式框实现
首先我们通过点击选中的符号和数字放到一个列表中,形成我们的公式列表:
比如: 0.33 + 1
的json格式如下
formulaItems = [
{
"uuid": "a039cd36-df4d-43e1-908d-52d264fcdeba",
"type": "num", // 常数
"value": "0.33"
},
{
"uuid": "6bf10a3d-8311-4de3-b049-2db8513a1165",
"type": "sign", // 符号
"value": "+"
},
{
"uuid": "4078a28f-144f-4041-9440-3d887234b13f",
"type": "num", // 常数
"value": "1"
}
]
利用edit-mock渲染公式中的每个小组件(数字/符号)
<template>
<div class="formula-textarea" @click.stop="handleBoxClick">
<edit-mock
ref="editMock"
class="formula-textarea__item"
v-for="(item, index) in formulaItems"
:id="item.uuid"
:key="item.uuid"
:noPre="index !== 0"
:compId="item.uuid"
@edit-keyup="handleDelete"
@edit-click="handleClick"
>
<!-- 渲染符号 ,imgSignClass是设置相应符号图:忽略-->
<span
v-if="item.type === 'sign'"
:class="['sign', imgSignClass(item.value, undefined, 'blue')]"
></span>
<!-- 渲染数字 -->
<p v-if="item.type === 'num'" class="num">{{item.value}}</p>
</edit-mock>
<!-- 没有公式的时候的 placeholder -->
<p v-if="itemLength <= 0" class="placeholder">{{placeholder}}</p>
</div>
</template>
<script>
import IndSelector from './IndSelector'
import EditMock from './EditMock'
import signClass from '@/views/dupont/sign-class.js'
export default {
name: 'FormulaTextarea',
mixins: [signClass],
components: {
IndSelector,
EditMock,
},
model: {
prop: 'formulaItems',
event: 'change',
},
props: {
formulaItems: {
type: Array,
default: () => [],
},
insertIdx: {
type: Number,
default: -1,
},
placeholder: {
type: String,
default: '可通过点击符号/常数来构造公式...'
}
},
data() {
return {
currentIdx: -1,
}
},
computed: {
itemLength() {
return this.formulaItems.length
},
},
methods: {
handleDelete({ compId, type }) {
const index = this.formulaItems.findIndex((item) => item.uuid === compId)
// 第一个backspace和最后一个delete事件不处理
const firstPre = index === 0 && type === 'pre'
const lastSub = index === this.itemLength - 1 && type === 'sub'
if (firstPre || lastSub) {
return
}
// 删除当前,前一个还是后一个
let delIndex = index
if (type === 'sub') {
delIndex = index + 1
} else if (type === 'pre') {
delIndex = index - 1
}
this.formulaItems.splice(delIndex, 1)
// 光标位置
const idx = type === 'sub' ? index + 1 : index
this.currentIdx = idx
},
// index 为insert的位置
setInputFocus(index) {
if (index >= 0 && this.itemLength > 0) {
this.$nextTick(() => {
const p = index === 0 ? 'preInput' : 'subInput'
const itemIdx = index - 1 < 0 ? 0 : index - 1
// XXX 只能通过id来找,refs中的顺序和实际顺序不一样
// this.$refs.editMock[itemIdx].$refs[p].focus()
const id = this.formulaItems[itemIdx].uuid
const node = this.$refs.editMock.find((item) => item.compId === id)
node.$refs[p].focus()
})
}
},
handleClick({ compId, type }) {
const index = this.formulaItems.findIndex((item) => item.uuid === compId)
const nowIndex = type === 'pre' ? index : index + 1
this.currentIdx = nowIndex
},
handleBoxClick() {
// 盒子点击,光标放到最后
this.currentIdx = this.itemLength
// 这个事件必须调用,没有改变时, watch监听不到, 如点击外部,再点击box的情况
this.setInputFocus(this.currentIdx)
},
},
watch: {
formulaItems: {
handler(v) {
// 为空时
if (!v || v.length === 0) {
this.currentIdx = -1
}
// 在首个插入没有设置光标
if(this.currentIdx === 0){
this.setInputFocus(0)
}
},
deep: true,
},
insertIdx(v) {
this.currentIdx = v
},
currentIdx(v) {
// 同步外部位置
this.$emit('update:insertIdx', v)
// 设置光标位置
this.setInputFocus(v)
},
},
}
</script>
<style lang="scss" scoped>
$itemheight: 24Px;
$numheight: $itemheight - 2Px;
.formula-textarea {
border: $border;
width: 100%;
height: 100%;
overflow: auto;
display: inline-flex;
flex-wrap: wrap;
align-content: flex-start;
.formula-textarea__item {
display: inline-flex;
margin: 10px 0px;
height: $itemheight;
line-height: $itemheight;
.sign {
margin-top: 2px;
}
.num {
display: inline-block;
@include font-size(14px);
height: $numheight;
line-height: $numheight;
padding: 0 8px;
background: $bg3;
border: $border;
border-radius: 2px;
}
.img-icon {
margin-right: 0;
}
}
.default-input {
width: 10px;
}
.placeholder {
color: $text-c2;
padding: 10px;
}
}
</style>
这里麻烦的一点在于光标位置的计算。
点击新增符号和数据, 这里主要注意新增时改变光标的位置:
/**
* 新增公式组件
*/
addFormulaItem(type, value, info) {
const uuid = generateUUID()
const res = { uuid, type, value }
if (info) {
res['info'] = info
}
// 定点插入和直接放到最后
if (this.insertIdx < 0) {
this.formulaItems.push(res)
} else {
this.formulaItems.splice(this.insertIdx, 0, res)
// 最前面插入无需加1
if(this.insertIdx !== 0) {
this.insertIdx += 1
}
}
},
addNum() {
this.addFormulaItem('num', this.numInput)
},
addSign(v) {
this.addFormulaItem('sign', v)
},
组件扩展
这里的数字和符号都看作组件处理,所以加个其他组件是很方便的: