用vue手写一个公式组件

2019-11-01 admin

因为最近接到一个需求,项目中要有一个公式编辑的模块,其中可能有手入公式和字段的功能,其他的可以进行手动修改。度娘、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,如有不对,还望不吝指正。

[转载]原文链接:https://segmentfault.com/a/1190000020876341

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-78741.html

文章标题:用vue手写一个公式组件

相关文章
Vue获取DOM元素样式和样式更改示例
在 vue 中用 document 获取 dom 节点进行节点样式更改的时候有可能会出现 ‘style’ is not definde的错误,这时候可以在 mounted 里用 $refs 来获取样式,并进行更改: &lt;template...
2017-03-13
YouTube正式默认使用HTML5视频播放器
YouTube视频网站现在默认使用HTML5播放器,这意味着更好的性能、 稳定性、 电池寿命和甚至是更好的安全性。现在用户通过Chrome、IE 11、Safari 8和Beta版本的Firefox进行浏览的时候都默认使用HTML5视频播放...
2015-11-12
JavaScript常用特效chm下载
下载地址:JavaScript常用特效chm下载 对了,如果打开空白,在手册上右键属性解除锁定即可。 ...
2015-11-12
零基础-5小时开发一个electron应用-[实践]
一、背景 三、技能升级 ​ 明明可以用颜值取胜,非要靠才华?不对,明明可以用代码搞定,非要搞设计?步入正题,正好最近对electron比较感兴趣,又是要做工具,那就直接怼 1.electron介绍 ​ electron最开始不叫这个名字,叫...
2017-12-26
使用jspdf生成pdf报表
由于前台html已经动态生成报表,而且,前台有一个功能,一个date range组件,当你拖动的时候,报表会在不提交到后台的情况下动态变化。 因此需要用到js生成生报表: 用到的组件: jquery.js jspdf.js canvg.js...
2017-03-25
使用axios发送post请求,body传送数据格式form和json区别
先来看看这两个种传送格式的写法 1.form格式,将Content-Type类型设置为application/x-www-form-urlencode,POST请求时将data序列化,提交的数据会按照 key1=val1&amp;key2=...
2018-07-25
梳理前端开发使用eslint-prettier检查和格式化代码
问题痛点 在团队的项目开发过程中,代码维护所占的时间比重往往大于新功能的开发。因此编写符合团队编码规范的代码是至关重要的,这样做不仅可以很大程度地避免基本语法错误,也保证了代码的可读性。 对于代码版本管理系统(svn 和 git或者其他)...
2018-05-07
jQuery中DOM树操作之使用反向插入方法实例分析
本文实例讲述了jQuery中DOM树操作之使用反向插入方法。分享给大家供大家参考。具体分析如下: 使用反向插入方法 这里我们先把创建的内容插人到元素前面,然后再把同一个元素插人到文档 中的另一个位置。通常,当在jQuery中操作元素时,利用...
2015-11-13
Bootstrap BootstrapDialog使用详解
这里有两种展现方式 写在前面:首先你要引入的库有 css : bootstrap.min.css bootstrap-dialog.css js : jquery-1.11.1.min.js bootstrap.min.js bootstr...
2017-03-16
React Native 用JavaScript编写原生ios应用
ReactNative 可以基于目前大热的开源JavaScript库React.js来开发iOS和Android原生App。而且React Native已经用于生产环境——Facebook Groups iOS 应用就是基于它开发的。 Re...
2015-11-12
回到顶部