通过div的contenteditable属性实现富文本自定义公式生成器

最近遇到一个需求,需要实现一个自定义公式生成器,找网上找了好久都没合适的,有找到类似的但是存在一些光标上的bug,让人非常头疼,最后参考了一些部分代码实现了自己的公式生成器,效果如下。

demo示例

难点就是在重置光标时的逻辑判断,如若不判断光标位置,会出现以下情况,点击已经生成的span导致光标消失,那么再点击生成span的按钮时,生成的span会插入到之前停留在span的位置,就会造成bug。

处理光标处的代码

/**
 * 失焦后重置光标位置,这里不重置位置,会造成bug,例如点击生成的span光标消失,再次点击生成span的按钮,会在最后光标停留的span标签里面再插入span,就会造成bug
 */
const onBlur = () => {
  selection.value = window.getSelection();
  range.value = selection.value?.getRangeAt(0);
  //如果最后的光标停留在text节点,那么就把光标移动至editor的最后面
  if (range.value.endContainer.nodeType === Node.TEXT_NODE) { // 检查结束节点是否为文本节点
    resetCursor()
  }
};

/**
 * 重置光标位置
 * */
const resetCursor = ()=>{
  const parentElement = document.getElementById('editor'); // 获取结束节点的父元素
  let ran = document.createRange();
  ran.selectNodeContents(parentElement);
  ran.collapse(false);
  let sel = window.getSelection();
  sel?.removeAllRanges();
  sel?.addRange(ran);
  range.value = sel.getRangeAt(0);
}

 demo源代码

<template>
  <div>
    <div class="top-buttons">
      <div>工具栏:</div>
      <el-button v-for="item in buttonList" type="primary" @click="getValue(item.value)">{{item.label}}</el-button>
    </div>
    <div
        ref="textRef"
        id="editor"
        :contenteditable="true"
        @blur="onBlur"
    ></div>
    <button @click="getSpan({label:'商品价格', value:'price' })">商品价格</button>
    <button @click="getSpan({label:'商品数量', value:'num' })">商品数量</button>
    <button @click="getTextAndParams">获取公式</button>
  </div>
</template>

<script setup>
const textRef = ref(null);
const selection = shallowRef(null)
const range = shallowRef(null)
const props = defineProps({
  isEdit: {
    type: Boolean,
    default: false
  },
  textValue:{
    type: String,
    default: ''
  }
})
const buttonList = [
  { label:'+', value:'+' },
  { label:'-', value:'-' },
  { label:'×', value:'*' },
  { label:'÷', value:'/' },
  { label:'<', value:'<' },
  { label:'>', value:'>' },
  { label:'>=', value:'>=' },
  { label:'<=', value:'<=' },
  { label:'=', value:'=' },
  { label:'()', value:'()' },
  { label:'%', value:'%' },
  { label:'与', value:'&' },
  { label:'或', value:'|' },
]
const dataList = [
  {label:'商品价格', value:'price' },
  {label:'商品数量', value:'num' }
]
/**
 * 失焦后重置光标位置,这里不重置位置,会造成bug,例如点击生成的span光标消失,再次点击生成span的按钮,会在最后光标停留的span标签里面再插入span,就会造成bug
 */
const onBlur = () => {
  selection.value = window.getSelection();
  range.value = selection.value?.getRangeAt(0);
  //如果最后的光标停留在text节点,那么就把光标移动至editor的最后面
  if (range.value.endContainer.nodeType === Node.TEXT_NODE) { // 检查结束节点是否为文本节点
    resetCursor()
  }
};

/**
 * 重置光标位置
 * */
const resetCursor = ()=>{
  const parentElement = document.getElementById('editor'); // 获取结束节点的父元素
  let ran = document.createRange();
  ran.selectNodeContents(parentElement);
  ran.collapse(false);
  let sel = window.getSelection();
  sel?.removeAllRanges();
  sel?.addRange(ran);
  range.value = sel.getRangeAt(0);
}

/**
 *  点击工具栏按钮添加文本节点
 */
const getValue = ( value ) => {
  // 创建一个文本节点
  const textNode = document.createTextNode(value);
  // 在光标位置插入文本节点
  range.value?.insertNode(textNode);
  // 移动光标到文本节点的末尾
  range.value?.setStartAfter(textNode);
  // 折叠光标到文本节点的末尾
  range.value?.collapse(true);
  // 移除所有选区 不移除selection会到聚焦点击的文本
  selection.value?.removeAllRanges();
  // 添加选区
  selection.value?.addRange(range.value);
};

/**
 *  点击参数生成span标签
 */
const getSpan = (params) => {
  // 创建前缀
  let prefix = `<span contenteditable="false" disabled="disabled" class="fn-param" data-param="${params.value}">`;
  // 创建后缀
  let suffix = "</span>";
  // 创建span元素
  let el = document.createElement("span");
  // 将前缀和后缀插入span元素
  el.innerHTML = prefix + params.label + suffix;
  // 去掉外层的span

  let frag = document.createDocumentFragment();
  let node = frag.appendChild(el.firstChild);

  // 插入tag
  range.value?.insertNode(node);
  // 设置光标
  range.value?.setStartAfter(node);
  range.value?.collapse(true);
  // 不移除selection会到聚焦点击的文本
  selection.value?.removeAllRanges();
  // 添加选区
  selection.value?.addRange(range.value);
};

/**
 * 获取构建的html里面的文本和参数
 */
const getTextAndParams = () => {
  // 获取文本中的参数元素
  let editor = document.getElementById('editor');
  let result = '';
  // 遍历编辑器的子节点,包括文本节点
  editor.childNodes.forEach(node => {
    if (node.nodeType === Node.TEXT_NODE) {
      result += node.textContent; // 获取文本节点内容
    } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SPAN') {
      result += node.dataset.param; // 获取 span 的 data-param 属性值
    }
  });

  // 返回文本
  let data = {
    value:result,
    label:textRef.value?.innerText
  }
  console.log(data)
  return data
};

onMounted(()=>{
  resetCursor()
  if(props.textValue){
    reviewFn(props.textValue)
  }
})
const reviewFn = (data) =>{
  // 拆分公式并处理每个部分
  const parts = data.split(/(\W)/); // 按照非字母字符分割公式
  console.log(parts)
  for (let i=0;i<parts.length;i++){
    let index = dataList.findIndex(item=>item.value===parts[i])
    if(index>-1){
      getSpan(dataList[index])
    }else{
      getValue(parts[i])
    }
  }
}
</script>

<style lang="scss" scoped>
#editor {
  width: 100%;
  height: 150px;
  padding: 10px;
  box-sizing: border-box;
  overflow: auto;
  background-color: #f5f5f5;
  border: 1px solid #ccc;
  border-radius: 5px;
  font-size: 20px;
  word-break: break-all;
  outline: none;
}
.top-buttons{
  margin: 5px;
  display: flex;
  align-items: center;
}
:deep(.fn-param){
  padding: 4px;
  background: #0e66b720;
  border-radius: 5px;
  color: #0e66b7;
  margin: 4px;
  display: inline-block;
}
</style>

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值