再看最后一眼青春的星空

灿烂火光就像盛夏的烟火

欢送挣扎万年文明的巅峰

我们啊

将变星辰永远飘在黑暗宇宙

这个男人来自三体

Tirion

导航

自定义编辑器实现光标处插入内容的功能


如图所示,需求是在光标处插入一个占位符,前台展示的时候将占位符替换为需要的内容。

思路

在文本输入框中插入占位符,首先想到的是 textarea,但是 textarea 有个问题:只能插入文本,就算插入了自定义的占位符,但是只是普通文本,用户可以对占位符进行编辑。而我们更希望的是插入的占位符用户不能编辑,删除也是整个占位符一起删除,而不是一个一个字符的删除。
所以,只能用富文本输入框来实现了。
富文本输入框的基本实现就是将一个 div 设置属性 contenteditable="true",这个 div 就可以在里面进行输入编辑了。
要插入占位符,首先需要的就是要获取到光标所在位置,那么该怎么做呢?这里主要就是使用 window.getSelection() 方法,这个方法主要用来获取光标选择的文本内容,就是按住鼠标不放在文本上拖动那种。但是即使光标没选择内容,也是具有光标信息的,可以通过这些信息来对光标进行操作。

下面是简单的获取光标 range 的代码,接收富文本框容器 dom 元素:


// 获取光标位置相关信息
function getCursorPosition(ele) {
  const doc = ele.ownerDocument || ele.document;
  const win = doc.defaultView || doc.parentWindow;
  const sel = win.getSelection();
  let range;
  if (sel.rangeCount > 0) {
    range = sel.getRangeAt(0);  // 获取到当前光标所在的元素区域对象

    // 光标不在富文本框内,则将 range 改为 undefined
    if (!ele.contains(range.startContainer)) {
      range = undefined;
    }
  }
  return range;
}

然后使用 range.insertNode() 方法就能在光标处插入自定义节点了,当然有些特殊情况要进行处理:

  const insertPlaceholder = (type) => {
    const range = getCursorPosition(editorRef);

    // 创建文本占位符
    const createPh = (type) => {
      let spanDom = document.createElement('span');
      spanDom.setAttribute('contentEditable', false);  // 占位符不能编辑
      spanDom.classList.add('editor_placeholder');
      spanDom.classList.add(typeMap[type].className);
      spanDom.innerText = `{&${type}&}`;
      return spanDom;
    }

    const placeholderDom = createPh(type);

    if (range) {  // 光标在富文本框内,插入到光标位置
      const rangeData = range.startContainer.data || '';
      if (/{&\w+&}/.test(rangeData)) {  // 光标在占位符上
        const focusPh = range.startContainer.parentElement;  // 获取占位符 dom
        setCursorAfter(focusPh);  // 光标设置到占位符后面
        range.insertNode(placeholderDom);
      } else {
        range.insertNode(placeholderDom);
      }
    } else {  // 插入到末尾
      editorRef.appendChild(placeholderDom);
    }

    // 光标移到插入的元素后面
    editorRef.focus();
    setCursorAfter(placeholderDom);
  }

想让占位符不能编辑只需将其 contenteditable 设为 false 即可。

完整 demo


const typeMap = {
  text: {
    // img: noData,
    className: 'editor_text',
  },
  num: {
    // img: img2,
    className: 'editor_num',
  },
  time: {
    // img: noData,
    className: 'editor_time',
  },
  percent: {
    // img: noData,
    className: 'editor_percent',
  },
};

// 获取光标位置相关信息
function getCursorPosition(ele) {
  const doc = ele.ownerDocument || ele.document;
  const win = doc.defaultView || doc.parentWindow;
  const sel = win.getSelection();
  let range;
  if (sel.rangeCount > 0) {
    range = sel.getRangeAt(0);  // 获取到当前光标所在的元素区域对象

    // 光标不在富文本框内,则将 range 改为 undefined
    if (!ele.contains(range.startContainer)) {
      range = undefined;
    }
  }
  return range;
}

// 设置光标为 ele 元素之后
function setCursorAfter(ele) {
  const sle = window.getSelection();
  const r = sle.getRangeAt(0)
  r.setStartAfter(ele);
  r.setEndAfter(ele)
}

export default (props) => {
  const [editorRef, setEditorRef] = useState(null);
  const [editorContent, setEditorContent] = useState('');

  const deleteListener = e => {
    if (e.key === 'Backspace' || e.key === 'Delete') {
      window.getSelection().getRangeAt(0).deleteContents();
    }
  }

  useEffect(() => {
    if (editorRef) {
      editorRef.addEventListener('keydown', deleteListener, false);
      return () => {
        editorRef.removeEventListener('keydown', deleteListener, false);
      }
    }
  }, [editorRef]);

  const onEditorChange = e => {
    setEditorContent(e.target.outerHTML);
  }

  const insertPlaceholder = (type) => {
    const range = getCursorPosition(editorRef);

    // 创建文本占位符
    const createPh = (type) => {
      let spanDom = document.createElement('span');
      spanDom.setAttribute('contentEditable', false);  // 占位符不能编辑
      spanDom.classList.add('editor_placeholder');
      spanDom.classList.add(typeMap[type].className);
      spanDom.innerText = `{&${type}&}`;
      return spanDom;
    }

    const placeholderDom = createPh(type);

    if (range) {  // 光标在富文本框内,插入到光标位置
      const rangeData = range.startContainer.data || '';
      if (/{&\w+&}/.test(rangeData)) {  // 光标在占位符上
        const focusPh = range.startContainer.parentElement;  // 获取占位符 dom
        setCursorAfter(focusPh);  // 光标设置到占位符后面
        range.insertNode(placeholderDom);
      } else {
        range.insertNode(placeholderDom);
      }
    } else {  // 插入到末尾
      editorRef.appendChild(placeholderDom);
    }

    // 光标移到插入的元素后面
    editorRef.focus();
    setCursorAfter(placeholderDom);
  }

  return (
    <div className={styles.container}>
      <div
          id="editor"
          className={styles.editor}
          contentEditable={true}
          onInput={onEditorChange}
          ref={ref => setEditorRef(ref)}
      />
      <div className={styles.btnsWrapper}>
        <div className={styles.insertBtn}>
          <Button type="primary" onClick={() => insertPlaceholder('text')}>插入文本填空</Button>
        </div>
        <div className={styles.insertBtn}>
          <Button type="primary" onClick={() => insertPlaceholder('num')}>插入数字填空</Button>
        </div>
        <div className={styles.insertBtn}>
          <Button type="primary" onClick={() => insertPlaceholder('time')}>插入时间填空</Button>
        </div>
        <div className={styles.insertBtn}>
          <Button type="primary" onClick={() => insertPlaceholder('percent')}>插入百分数填空</Button>
        </div>
      </div>
    </div>
  )
}

posted on 2021-05-31 16:13  Tirion  阅读(621)  评论(0编辑  收藏  举报

The Man from 3body