自定义公式输入框

需求

请添加图片描述
自定义公式输入框,要求:
1、通过点击的方式添加数据和符号;
2、自定义输入框中可以像普通输入框那样,在光标位置插入输入;
3、用户通过delete和backspace按键可以删除;
4、点击输入框,光标定位到最后;

npm直接使用

地址:element-textarea

//安装
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)
 },
组件扩展

这里的数字和符号都看作组件处理,所以加个其他组件是很方便的:
请添加图片描述

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值