用vue手写一个公式组件

41次阅读

共计 6548 个字符,预计需要花费 17 分钟才能阅读完成。

因为最近接到一个需求,项目中要有一个公式编辑的模块,其中可能有手入公式和字段的功能,其他的可以进行手动修改。度娘、github 了好久未找到好的轮子,没有办法,只能自己写一个了,实现基本功能。

废话不多说,直接上代码,因为是个 demo 所以一些公式和字段是手上去的,后面如果需要可以再进行细节优化。

<template>
  <div id="formulaPage">
    <h1>formulaPage</h1>
    <p>{{formulaStr}}</p>
    <div class="btnGroup">
      <!-- <button @click="mouseRange($event)"> 获取光标 </button> -->
      <button @click="getFormula"> 获取公式 </button>
      <button @click="parsingFormula('#字段 1#+plus(#字段 1#+#字段 3#)*abs(#字段 4#/# 字段 2#)')"> 反向解析公式 </button>
    </div>
    <div class="tab">
      <div class="tit"> 添加公式 </div>
      <ul>
        <li @click="addItem($event, 2)">plus()</li>
        <li @click="addItem($event, 2)">abs()</li>
      </ul>
    </div>
    <div class="tab">
      <div class="tit"> 添加字段 </div>
      <ul>
        <li @click="addItem($event, 1)"> 字段 1 </li>
        <li @click="addItem($event, 1)"> 字段 2 </li>
        <li @click="addItem($event, 1)"> 字段 3 </li>
        <li @click="addItem($event, 1)"> 字段 4 </li>
      </ul>
    </div>
    <!-- 公式编辑区域 -->
    <div 
      class="formulaView" 
      ref="formulaView" 
      contentEditable='true' 
      @click="recordPosition"
      @keyup="editEnter($event)"
      @copy="copy($event)"
      @paste="paste($event)"
    ></div>
  </div>
</template>

style

<style lang="less">
  #formulaPage{
    >.tab{
      >ul{
        &:after{
          content: '';
          display: table;
          clear: both;
        }
        >li{
          margin-right: 20px;
          float: left;
          padding: 0 10px;
          height: 25px;
          line-height: 25px;
          border-radius: 5px;
          border: 1px solid #000;
        }
      }
    }
    >.formulaView{
      margin-top: 20px;
      min-height: 100px;
      width: 300px;
      padding: 5px;
      border: 2px solid #000;
      resize: both;
      overflow: auto;
      line-height: 25px;
      span{
        user-select: none;
        display: inline-block;
        margin: 0 3px;
        height: 20px;
        line-height: 20px;
        letter-spacing: 2px;
        background: #aaa;
        border-radius: 3px;
        white-space: nowrap;
        color: red;
        &:first-child{margin-left: 0;}
      }
    }
  }
</style>

js

<script>
export default {
  name: 'formulaPage',
  data: function () {
    return {
      // 公式字符串
      formulaStr:'',
      // 公式编辑器最后光标位置
      formulaLastRange: null,
    }
  },
  methods: {
    // 获取公式
    getFormula: function(){
      var nodes = this.$refs.formulaView.childNodes;
      var str = "";
      for(let i=0;i<nodes.length;i++){var el = nodes[i];
        if(el.nodeName=="SPAN"){// console.log(el);
          str+='#'+el.innerHTML.trim()+'#';}else{// console.log(el.data);
          str+=el.data?el.data.trim():'';}
      }
      // console.log(str);
      this.formulaStr = str;
    },
    // 点选时记录光标位置
    recordPosition: function () {
      // 保存最后光标点
      this.formulaLastRange = window.getSelection().getRangeAt(0);
    },
    // 添加字段 type 1 字段  2 公式
    addItem: function (e, type) {
      
      // 当前元素所有子节点
      var nodes = this.$refs.formulaView.childNodes;
      // 当前子元素偏移量
      var offset = this.formulaLastRange && this.formulaLastRange.startOffset;
      // 当前光标后的元素
      var nextEl = this.formulaLastRange && this.formulaLastRange.endContainer;

      // 创建节点片段  
      var fd = document.createDocumentFragment();
      // 创建字段节点  空白间隔节点  公式节点
      var spanEl = document.createElement("span");
      spanEl.setAttribute('contentEditable',false);
      // 标识为新添加元素 用于定位光标
      spanEl.setAttribute('new-el',true);
      spanEl.innerHTML = e.target.innerHTML;
      var empty = document.createTextNode(' ');
      var formulaEl = document.createTextNode(''+e.target.innerHTML+' ');

      // 区分文本节点替换 还是父节点插入
      if(nextEl && nextEl.className != 'formulaView'){
        // 获取文本节点内容
        var content = nextEl.data;

        // 添加前段文本
        fd.appendChild(document.createTextNode(content.substr(0,offset)+' '));
        fd.appendChild(type==1?spanEl:formulaEl);
        // 添加后段文本
        fd.appendChild(document.createTextNode(' '+content.substr(offset)));
        // 替换节点
        this.$refs.formulaView.replaceChild(fd, nextEl);
          
      }else{
        // 添加前段文本
        fd.appendChild(empty);
        fd.appendChild(type==1?spanEl:formulaEl);
        fd.appendChild(empty);

        // 如果有偏移元素且不是最后节点  中间插入节点  最后添加节点
        if(nodes.length && nodes.length>offset){
          this.$refs.formulaView.insertBefore( fd, 
            (nextEl&& nextEl.className!= 'formulaView')? nextEl:nodes[offset]
          );
        }else{this.$refs.formulaView.appendChild(fd);
        }
      }


      // 遍历光标偏移数据 删除标志
      var elOffSet = 0;
      for(let i = 0 ;i < nodes.length; i++){let el = nodes[i];
        // console.log(el,el.nodeName == 'SPAN'&&el.getAttribute('new-el'));
        if(el.nodeName == 'SPAN' && el.getAttribute('new-el')){
          elOffSet = i;
          el.removeAttribute('new-el');
        }
      }

      // 创建新的光标对象
      var range = document.createRange()
      // 光标对象的范围界定
      range.selectNodeContents(type==1?this.$refs.formulaView:formulaEl);
      // 光标位置定位 
      range.setStart(
        type==1?this.$refs.formulaView:formulaEl, 
        type==1?elOffSet + 1:formulaEl.data.length-2
      );

      // 使光标开始和光标结束重叠
      range.collapse(true)
      // 清除选定对象的所有光标对象
      window.getSelection().removeAllRanges()
      // 插入新的光标对象
      window.getSelection().addRange(range);

      // 保存新光标
      this.recordPosition();},
    // 复制
    copy: function (e) {
      // 选中复制内容
      e.preventDefault();
      //
      var selContent = document.getSelection().toString().split("\n")[0];
      // 替换选中内容
      e.clipboardData.setData('text/plain', selContent);
    },
    // 输入回车
    editEnter: function (e) {// console.log(e);
      e.preventDefault();
      
      // return '<br/>';
      // return
      if(e.keyCode == 13){
        // 获取标签内容 并把多个换行替换成 1 个
        var content = this.$refs.formulaView.innerHTML.replace(/(<div><br><\/div>){2,2}/g, "<div><br></div>");

        // debugger;

        // 记录是否第一行回车
        var divCount = this.$refs.formulaView.querySelectorAll("div");
        // var tE = this.$refs.formulaView.querySelect('div');
        // console.log(this.$refs.formulaView.childNodes);
        // console.log(this.$refs.formulaView.querySelectorAll("div"));
        // 获取当前元素内所有子节点
        var childNodes = this.$refs.formulaView.childNodes;
        // 记录当前光标子节点位置
        var rangeIndex = 0;
        for(let i = 0 ; i < childNodes.length ; i++){var one = childNodes[i];
          if(one.nodeName == 'DIV'){rangeIndex = i;}
        }
        // console.log(rangeIndex);
        // debugger;
        // console.log(content);

        // 如果有替换则进行光标复位
        if(divCount.length >= 1){
          // 替换回车插入的 div 标签
          content = content.replace(/<div>|<\/div>/g,function(word){// console.log(word);
            if(word == "<div>"){
              // 如果是第一行不在替换 br
              return divCount.length>1?'':' <br>';
            }else if(word == '</div>'){return ' ';}
          });
          // 更新替换内容,光标复位
          this.$refs.formulaView.innerHTML = content;
          // 创建新的光标对象
          var range = document.createRange()
          // 光标对象的范围界定为新建的表情节点
          range.selectNodeContents(this.$refs.formulaView)
          // 光标位置定位在表情节点的最大长度
          range.setStart(this.$refs.formulaView, rangeIndex+(divCount.length>1?0:1));
          // 使光标开始和光标结束重叠
          range.collapse(true)
          // 清除选定对象的所有光标对象
          window.getSelection().removeAllRanges()
          // 插入新的光标对象
          window.getSelection().addRange(range);
        }

      }
      // 保存最后光标点
      this.formulaLastRange = window.getSelection().getRangeAt(0);

    },
    // 获取粘贴事件
    paste: function(e){e.preventDefault();
      // var txt=e.clipboardData.getData();
      // console.log(e, e.clipboardData.getData());
      return "";
    },
    // 公式反向解析
    parsingFormula: function(formulaStr){
      // 渲染视口
      var view = this.$refs.formulaView;
      // 反向解析公式
      var str = formulaStr.replace(/#(.+?)#/g,function(word,$1){// console.log(word,$1);
        return "<span contentEditable='false'>"+$1+"</span>"
      });

      // console.log(str,fd.innerHTML);
      view.innerHTML = str;
      // this.$refs.formulaView.appendChild(fd);

      // 创建新的光标对象
      var range = document.createRange()
      // 光标对象的范围界定为新建的表情节点
      range.selectNodeContents(view)
      // 光标位置定位在表情节点的最大长度
      range.setStart(view, view.childNodes.length);

      // 使光标开始和光标结束重叠
      range.collapse(true)
      // 清除选定对象的所有光标对象
      window.getSelection().removeAllRanges()
      // 插入新的光标对象
      window.getSelection().addRange(range);

      // 保存新光标
      this.recordPosition();},
  }
}
</script>

思路:
因为字段是不允许编辑的,所以采用的是元素编辑功能,设置元素属性 contentEditable=’true’ 可以对元素进行编辑。
子元素如果不想被编辑可以设置为 false。如果不设置此属性会被继承。
在其间遇到不少坑,比如回车后,会自动在元素内解析成 <div></div> 元素包裹,所以我会对回车进行内容进行正则匹配过滤。
另外,当比较麻烦的是对内容进行添加字段和公式后如何进行光标复位。这块是借鉴 https://segmentfault.com/a/1190000005869372;
只是一个小小的 Demo,如有不对,还望不吝指正。

正文完
 0