用vue手写一个公式组件

因为最近接到一个需求,项目中要有一个公式编辑的模块,其中可能有手入公式和字段的功能,其他的可以进行手动修改。度娘、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 。如果不设置此属性会被继承。 在其间遇到不少坑,比如回车后,会自动在元素内解析成

元素包裹,所以我会对回车进行内容进行正则匹配过滤。 另外,当比较麻烦的是对内容进行添加字段和公式后如何进行光标复位。这块是借鉴https://segmentfault.com/a/1190000005869372; 只是一个小小的Demo,如有不对,还望不吝指正。

原文链接:segmentfault.com

上一篇:JS写不一样的悬停几秒后执行函数
下一篇:HooX: 应该不是 React 下一代状态管理工具

相关推荐

  • 🔥手写大厂前端知识点源码系列(上)

    如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。

    2 个月前
  • 🔥基于vue3.0.1 beta搭建仿京东淘宝的电商商城项目!

    前言 就在前段时间,vue官方发布了3.0.0beta.1 版本,趁着五一假期有时间,就把之前的一个电商商城的项目,用最新的Composition API拿来改造一下! 👉GitHub地址请访问🔗...

    1 个月前
  • 🔥前端面试大厂手写源码系列(上)

    如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。

    2 个月前
  • 🔥0202年了,几个基础的手写函数总得会吧

    这几天看到一个大三大佬面试字节跳动的蚊子,突然觉得自己太辣鸡了···校招的题我一半多都不会啊···赶紧潜下心来学习学习提(an)高(wei)自己,边翻掘金边谷歌,简单实现了几个常用函数···(借鉴了太...

    2 个月前
  • (源码分析)为什么 Vue 中 template 有且只能一个 root ?

    引言 今年,疫情并没有影响到各种面经的正常出现,可谓是络绎不绝(学不动...)。然后,在前段时间也看到一个这样的关于 Vue 的问题,为什么每个组件 template 中有且只能一个 root? 可能...

    2 个月前
  • (小白篇)vue-cli3.0创建项目+引入element-ui

    vuecli在2018年8月份发布了3.0版本,经过重构之后,可以说是一个船心版本! 在项目都落地之后,就想升级一下cli版本,尝一尝3.0带来的舒适,也是为后面项目的开展做一个准备。

    1 年前
  • (小小黑科技)vue+echarts实现半圆图表

    如何用echarts实现半圆图表?在echarts官方实例倒腾一波,发现官方并没有提供半圆图表的写法,那怎么办呢?官方没提供,但需求还是要实现的。 半圆图表其实就是饼图的一半,那么简单的思路如下:设...

    1 年前
  • (原创)vue-router的Import() 异步加载模块问题的解决方案

    关注不迷路,如果解决了问题,留下个赞。 1、问题现象 (/public/upload/e221e3db24c3f24a41062b6e4e389df8) 2、出现问题的代码点 (/publ...

    18 天前
  • (vue框架)为element组件赋初始值以后无法更改值得问题

    情况描述:组件未加载时已有初始值,mounted里面加载数据,赋值,渲染以后,组件无法更改内容 data里面已经有这个表单对象的初始值但还是无法修改,之前有过一次,没有给表单绑定对象,所以赋值以后无法...

    1 年前
  • (vuejs学习)2、使用ElementUI(*)

    1.element安装 开发环境是win10,一到node官网下载node的.msi包(https://npm.taobao.org/mirrors/node/v10.16.0/nodev10.16....

    10 个月前

官方社区

扫码加入 JavaScript 社区