由於最近接到一個需求,項目中要有一個公式編輯的模塊,其中可能有手入公式和字段的功能,其餘的能夠進行手動修改。度娘、github了很久未找到好的輪子,沒有辦法,只能本身寫一個了,實現基本功能。node
廢話很少說,直接上代碼,由於是個demo因此一些公式和字段是手上去的,後面若是須要能夠再進行細節優化。git
<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>
stylegithub
<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>
jssegmentfault
<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> 元素包裹,因此我會對回車進行內容進行正則匹配過濾。
另外,當比較麻煩的是對內容進行添加字段和公式後如何進行光標復位。這塊是借鑑http://www.javashuo.com/article/p-gkbgrjdf-ge.html;
只是一個小小的Demo,若有不對,還望不吝指正。app