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