因爲最近接到一個需求,項目中要有一個公式編輯的模塊,其中可能有手入公式和字段的功能,其他的可以進行手動修改。度娘、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,如有不對,還望不吝指正。