如图所示,需求是在光标处插入一个占位符,前台展示的时候将占位符替换为需要的内容。
思路
在文本输入框中插入占位符,首先想到的是 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>
)
}