前端 - 用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;
一、获取组件内的输入值
- 如果外层有包裹form,那么可以直接调用form的内置方法,在各个值会改变的地方调用 onChange,外层form就可以直接拿到value。
- 如果外层没有包裹form,那么可以用 useImperativeHandle 和 forwardRef 向上传递value,父组件可以通过ref拿到子组件的值。
二、ContentEditable
- contentEditable: “true”,元素可编辑,可输入html标签
- contentEditable: “plaintext-only”,编辑区域只能键入纯文本
最明显的差异就是,属性设置true,回车会显示 <br > ,如果属性设置 plaintext-only,回车会显示换行符
三、onCompositionStart/onCompositionEnd
如果需要实时监听输入的数据,那么输入中文的时候,就会发现会将中文拼音也会记录,这时候就需要 onCompositionStart 、onCompositionEnd 再加上变量判断,等到中文输入完成之后,再处理输入的中文,中间输入的中文拼音忽略不处理。