用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 。如果不設置此屬性會被繼承。
在其間遇到不少坑,比如回車後,會自動在元素內解析成 <div></div> 元素包裹,所以我會對回車進行內容進行正則匹配過濾。
另外,當比較麻煩的是對內容進行添加字段和公式後如何進行光標復位。這塊是借鑑https://segmentfault.com/a/1190000005869372;
只是一個小小的Demo,如有不對,還望不吝指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章