前端 - 用div仿输入框,解决鼠标点击位置错乱的问题

由于项目需要在输入框的中插入各种自定义标签,特别的,需要将自定义标签插入指定的位置,可能是已有的字符串中间,而现有的组件无法记录失去焦点前的鼠标位置,所以采用div仿造一个div来实现需求,该方案可于移动端和pc端

直接上代码

import React, { useEffect, useState, useRef } from "react";
import styles from "./index.scss";

function DiscussInput({ tagName, value, wrapStyle = {} }, ref) {
  const [caretOffset, setCaretOffset] = useState(0);
  const [focusNode, setFocusNode] = useState(null);

  const inputElement = useRef(null);

  // in order to form.resetFields()
  useEffect(() => {
    if (!value) {
      inputElement.current.innerHTML = "";
    }
  }, [value]);

  useEffect(() => {
    if (tagName) {
      inputElement.current.focus();
      setCaretPosition();

      insertContent(tagName);
    }
  }, [tagName]);

  function setCaretPosition() {
    if (document.createRange && focusNode) {
      const range = document.createRange();
      range.setStart(focusNode, caretOffset);
      range.setEnd(focusNode, caretOffset);
      const selection = window.getSelection();
      selection.removeAllRanges();
      selection.addRange(range);
    }
  }

  function getCursortPosition(element) {
    const selection = window.getSelection();
    if (selection.rangeCount > 0) {
      const range = selection.getRangeAt(0);
      setCaretOffset(range.endOffset);
    }
  }

  function insertContent(content) {
    if (!content) {
      return;
    }
    inputElement.current.focus();
    let sel = null;
    sel = window.getSelection();

    if (sel.rangeCount > 0) {
      let range = sel.getRangeAt(0); //获取选择范围
      range.deleteContents(); //删除选中的内容
      let frag = document.createDocumentFragment(); //创建一个空白的文档片段,便于之后插入dom树
      let lastNode = frag.appendChild(content);
      range.insertNode(frag); //设置选择范围的内容为插入的内容
      let contentRange = range.cloneRange(); //克隆选区
      contentRange.setStartAfter(lastNode); //设置光标位置为插入内容的末尾
      contentRange.collapse(true); //移动光标位置到末尾
      sel.removeAllRanges(); //移出所有选区
      sel.addRange(contentRange); //添加修改后的选区
    }
  }

  let inputFlag = true;

  return (
    <div
      className={styles["topic-input"]}
      style={{ ...wrapStyle }}
      onClick={() => {
        inputElement.current.focus();
      }}
    >
      <div
        className={styles["text"]}
        ref={inputElement}
        contentEditable="plaintext-only"
        onFocus={e => {
          const range = window.getSelection();
          range.selectAllChildren(e.target);
          range.collapseToEnd();
        }}
        onBlur={e => {
          const node = window.getSelection().focusNode;
          getCursortPosition(node);
          setFocusNode(node);
        }}
        // onInput={e => {
        //   const target = e.currentTarget;
        //   setTimeout(() => {
        //     if (inputFlag) {
        //       onChange(target.innerHTML);
        //     }
        //   }, 0);
        // }}
        onCompositionStart={e => {
          inputFlag = false;
        }}
        onCompositionEnd={e => {
          inputFlag = true;
        }}
      />
    </div>
  );
}

export default DiscussInput;

一、获取组件内的输入值
  1. 如果外层有包裹form,那么可以直接调用form的内置方法,在各个值会改变的地方调用 onChange,外层form就可以直接拿到value。
  2. 如果外层没有包裹form,那么可以用 useImperativeHandle 和 forwardRef 向上传递value,父组件可以通过ref拿到子组件的值。
二、ContentEditable
  1. contentEditable: “true”,元素可编辑,可输入html标签
  2. contentEditable: “plaintext-only”,编辑区域只能键入纯文本

最明显的差异就是,属性设置true,回车会显示 <br > ,如果属性设置 plaintext-only,回车会显示换行符

三、onCompositionStart/onCompositionEnd

如果需要实时监听输入的数据,那么输入中文的时候,就会发现会将中文拼音也会记录,这时候就需要 onCompositionStart 、onCompositionEnd 再加上变量判断,等到中文输入完成之后,再处理输入的中文,中间输入的中文拼音忽略不处理。