实现输入框支持插入 带css样式的html标签

511 阅读3分钟

需求 :在textArea输入框插入一段带样式的文本

之前遇到一个场景,想了下这不就是简易版的富文本吗,用富文本库可以实现但是太大材小用了,还是用css 实现可编辑div + JS Range 对象定位光标实现即可

关于css -webkit-user-modify

这是个css属性,通过对应属性的设置也可以达到同样的效果。

read-only: 默认值,元素只读,不可编辑;

read-write: 可以编辑,支持富文本;

read-write-plaintext-only: 可以编辑,不支持富文本;

write-only: 使元素仅用于编辑(几乎没有浏览器支持)

由于还需根据光标的位置插入标签,所以我们还需要这么一个对象Range。

可以用 Document 对象的 Document.createRange 方法创建 Range,也可以用 Selection 对象的 getRangeAt 方法获取 Range。另外,还可以通过 Document 对象的构造函数 Range() 来得到 Range。我在实现的时候通过的是selection对象(表示用户选择的文本范围或插入符号的当前位置)的方法创建的,通过监听selectionchange事件来响应式的更新我的range,这样我就可以定位到光标的位置,那么对于标签插在哪的问题就解决了。

效果图

image.png

代码实现如下(方案一)


<template>
  <div
    @mouseenter="mouseenter"
    @mouseleave="mouseleave"
    class="myTextArea"
     id="edit"
     ref="innerRuleRef"
    cols="30"
    rows="10"
  ></div>

  <div>
    <Button @click="addfactor"> 插入元素 {{ isEnter }}</Button></div
  >
</template>
<script lang="ts" setup>
  import { ref } from 'vue';
  import { Button } from '/@@/Button';

  const Range = ref<any>(null);
  const selecthandler = () => {
    if (isEnter.value) {
      const sel = window.getSelection();
      Range.value = sel ? (sel.rangeCount > 0 ? sel?.getRangeAt(0) : null) : null;
    }
  };
  const isEnter = ref(false);
  const addTag = (text: string) => {
    const node = document.createElement('wise');
    node.innerText = text;
    // const cancelNode = document.createElement('span');
    // cancelNode.innerText = '✕';
    // cancelNode.style.color = 'black';
    // node.append(cancelNode);
    // document.execCommand('insertHTML', false, '<div style="color:red"> test </div>');
    Range.value?.insertNode(node);
    // insertNode(node);
  };
  const mouseleave = () => {
    isEnter.value = false;
  };
  const mouseenter = () => {
    isEnter.value = true;
  };
  document.addEventListener('selectionchange', selecthandler);
  // 添加因子
  const addfactor = () => {
    addTag('test');
  };
</script>

<style>
  .myTextArea {
    -webkit-user-modify: read-write-plaintext-only !important;
    border: 1px solid #ccc;
    overflow: hidden;
    box-sizing: border-box;
    word-break: break-word;
    height: 200px;
    width: 200px;
  }
  wise {
    background-color: #f0f6fe;
    background-color: #5387f7;
    padding: 0 1px;
    border-radius: 2px;
    /* white-space: nowrap; */
    cursor: default;
    -webkit-user-modify: read-only !important;
    margin-left: 8px;
  }
</style>

以上我们已经可以简易实现我们需求了 ,我们通过isEnter 来判断鼠标是否进入 可编辑输入框区域,如果不加这层判断,那么鼠标点到按钮插入元素时,其实光标也定位到按钮这里了 就会在按钮处插入标签,这不是我们想要的,但是用@mouseenter="mouseenter" @mouseleave="mouseleave" 来判断不太优雅,我们开源用Range 自带的属性来判断光标是否进入可编辑区域

代码实现如下(方案二)


/** 获取光标,插入html */


<template>
  <div
    @mouseenter="mouseenter"
    @mouseleave="mouseleave"
    class="myTextArea"
     id="edit"
     ref="innerRuleRef"
    cols="30"
    rows="10"
  ></div>

  <div>
    <Button @click="addfactor"> 插入元素 {{ isEnter }}</Button></div
  >
</template>

<script lang="ts" setup>
  import { ref } from 'vue';
  import { Button } from '/@@/Button';
  
  const addTag = (text: string) => {
    const node = document.createElement('wise');
    node.innerText = text;
    Range.value?.insertNode(node);
    pasteHtmlAtCaret(node)
  };
  const addfactor = () => {
    addTag('test');
  };
  
  
  pasteHtmlAtCaret = (html: any) => {
  const sel = window.getSelection();

  if (!sel) return;

  let range: any;

  if (sel && sel.rangeCount) {
    range = sel.getRangeAt(0);
  }
  // debugger;
  if (['', null, undefined].includes(range)) {
    // 如果div没有光标,则在div内容末尾插入
    range = keepCursorEnd(true)?.getRangeAt(0);
  } else {
    const targetNode = document.getElementById('edit');
    const contentRange = document.createRange();
    contentRange.selectNode(targetNode as HTMLElement);
    // 对比range,检查光标是否在输入范围内
    const compareStart = range.compareBoundaryPoints(Range.START_TO_START, contentRange);
    const compareEnd = range.compareBoundaryPoints(Range.END_TO_END, contentRange);
    const compare = compareStart !== -1 && compareEnd !== 1;
    if (!compare) range = keepCursorEnd(true)?.getRangeAt(0);
  }

  console.log(range);
  // debugger;
  // 末尾添加空格
  // html += '&nbsp;';
  const input = range.createContextualFragment(html);
  // 记录插入input之后的最后节点位置
  const lastNode = input.lastChild;

  range.insertNode(input);
  // 如果有最后的节点
  if (lastNode) {
    range = range.cloneRange();
    range.setStartAfter(lastNode);
    range.collapse(true);
    sel.removeAllRanges();
    sel.addRange(range);
  }
};

/** 将光标重新定位到内容最后 */
 keepCursorEnd = (isReturn: any) => {
  const targetNode = document.getElementById('edit');
  targetNode?.focus();
  // 创建range
  const sel = window.getSelection() as Selection;
  // range 选择obj下所有子内容
  sel.selectAllChildren(targetNode as HTMLElement);
  // 光标移至最后
  sel.collapseToEnd();

  if (isReturn) return sel;
};
  
  
</script>