需求
当textArea或者已有的富文本编辑器不能满足业务需求时,需要特殊定制该业务的多行文本编辑器。如:左边显示文本行数,右边分行编辑对应数据
分析
数据是一串数组,要把数组中的内容按照如上图所示展示,同时像textArea一样编辑,编辑后的结果以数组的形式给后端。
- 给多行p标签添加contentEditable=’true’属性,实现对应界面的展示
- 键盘事件的点击的操作,实现对光标以及数据的处理:如点击删除、上、下、enter等按键
实现(此处只展示部分关键代码)
1.拿到对应的数据并渲染到界面上,react中需要suppressContentEditableWarning=’true’排除警告,关键代码如下所示:
this.state = {
wordsList: [
'好无聊啊,我们玩点游戏吧',
'嗯好啊,玩什么呢?',
'我夸你一句,你夸我一句怎么样?',
'可以啊!你先说',
'你真漂亮!',
'你眼光真好!',
'!!!',
],
}
{this.state.wordsList.map((value, index) => {
return (
<div>
<span>
{index}
</span>
<div
id={index}
contentEditable="true"
suppressContentEditableWarning="true"
onKeyDown={(e) => {
this.onKeyDown(e);
}}
onKeyUp={(e) => {
this.onKeyUp(e);
}}
onMouseDown={() => {
this.setIndex(index);
}}
onClick={(e) => {
this.onClick(e);
}}
>
{value}
</div>
</div>
);
})}
复制代码
2.当键盘点击时,根据keycode的值判断点击的哪个按键并处理数据和光标。
点击某一行文本时,记录当前点击时光标所在位置(为后续键盘处理上下按键时所需要的)
比如:当我点击到第四行的‘先’和‘说’中间时
再去按两次键盘‘下’方向键时,若该行文字长度小于光标所在位置,则光标处于最末尾,若该行文本内容大于所记录的光标位置,则光标处于光标所记录的位置。
onClick(e) {
let sel = window.getSelection(); // 获取光标
let range = sel.getRangeAt(0);
this.setState({
recordIndex: range.startOffset
})
}
复制代码
当点击上下(键盘码:38、40)按键时,需要获取上/下行dom元素,创建range范围对象,上/下行文本长度与当前光标所在位置进行比较,并设置光标所在行和位置
if (e.keyCode == 38) {
e.preventDefault(); // 阻止向上箭头按键的默认事件
this.setCursor(index - 1)
} else if (e.keyCode == 40) {
e.preventDefault(); // 阻止向上箭头按键的默认事件
this.setCursor(index + 1)
}
setCursor(domIndex) {
let txtFocus = document.getElementById(domIndex); // 获取上/下行id的dom,并判断是否存在
if (!txtFocus) {
return
}
const range = document.createRange()
// 上/下行文本是否小于当前光标所在位置,并设置光标位置
if (txtFocus.innerText.length < this.state.recordIndex) {
range.setStart(txtFocus.firstChild, txtFocus.innerText.length)
range.setEnd(txtFocus.firstChild, txtFocus.innerText.length)
} else {
range.setStart(txtFocus.firstChild, this.state.recordIndex)
range.setEnd(txtFocus.firstChild, this.state.recordIndex)
}
const sel = window.getSelection()
sel.removeAllRanges()
sel.addRange(range)
this.setState({
// 更新index的指向第几行
index: domIndex,
});
}
复制代码
同理,当点击左/右键(键盘码:37、39)时,需要设置对应光标所在位置。此时需要特别留意光标在该行首尾时,再次点击左/右按键需要换行
当点击enter按键时(键盘码:13),需要判断光标位置是否在这行文字的最末尾,若是在最末尾,则需在后面增加一行,若在改行文本中间,需要把光标后面的内容放到下一行,并处理光标。部分代码如下
let sel = window.getSelection(); // 获取光标
let range = document.createRange();
range = sel.getRangeAt(0);
if (e.keyCode == 13) {
// 对enter按键进行处理,enter按键码:13
e.preventDefault(); // 阻止enter按键的默认事件
if (range.startOffset < innerText.length) {
// 判断光标位置是否在这行文字的最末尾
wordsList.splice(nextIndex, 0, innerText.substring(range.startOffset));
wordsList[index] = innerText.substring(0, range.startOffset);
} else {
wordsList.splice(nextIndex, 0, ""); // 向wordsList数组中添加一项
}
let txtFocus = document.getElementById(nextIndex); // 获取id下一行的dom
txtFocus.focus(); // 光标显示在下一行,聚焦的作用
this.forceUpdate(); // state修改,强制刷新页面数据
this.setState({
// 更新index的指向位置
index: nextIndex,
});
}
复制代码
当点击删除按键(按键码:8)时,需要判断当前光标所在位置是否为行首,若为行首,则需要对改行内容进行换行处理,若不在行首则删除对应的文字并处理光标,下面代码不严谨,切勿照搬照抄
if (e.keyCode == 8) {
// 对backspace按键进行处理,按键码:8
if (range.startOffset == 0) {
e.preventDefault(); // 阻止enter按键的默认事件
if (innerText.length == 0) {
// 如果这一行文本的内容为空时
wordsList.splice(index, 1);
// txtFocus.focus(); // 光标显示在下一行,聚焦的作用
// 处理光标问题
if (window.getSelection) { //ie11 10 9 ff safari
txtFocus.focus(); //解决ff不获取焦点无法定位问题
let range1 = window.getSelection(); //创建range
range1.selectAllChildren(txtFocus); //range 选择obj下所有子内容
// range1.modify('extend','right',2)
range1.collapseToEnd(); //光标移至最后
} else if (document.selection) {//ie10 9 8 7 6 5
let range1 = document.selection.createRange(); //创建选择对象
range1.moveToElementText(range1); //range定位到obj
range1.collapse(false); //光标移至最后
range1.select();
}
this.forceUpdate(); // state修改,强制刷新页面数据
this.setState({
// 更新index的指向位置
index: prIndex,
});
} else {
innerText1 = innerText1.concat(innerText);
// 如果这一行文本的内容不为空时
wordsList.splice(index, 1);
this.forceUpdate(); // state修改,强制刷新页面数据
wordsList[prIndex] = innerText1;
this.setState({
// 更新index的指向位置
index: prIndex,
}, () => {
// 处理光标问题, 此处必须在设置state以后方可
range.setStart(txtFocus.firstChild, innerText1.length - innerText.length)
range.setEnd(txtFocus.firstChild, innerText1.length - innerText.length)
sel.removeAllRanges()
sel.addRange(range)
});
}
}
}
复制代码
3.其他按键处理:后续可以对del,pgUp,pgDn,ctrl+s,ctrl+v等键盘事件进行相应处理;ctrl+s这种多键盘事件,可以根据键盘事件中event的ctrlKey来判断是否点击了ctrl。
其他
该案例的处理还有很多不到位的地方,比如对浏览器兼容的处理,后续有时间再做更改。
看有些富文本编辑器的光标采用自定义光标的方式,比如掘金写文章这里。这些等待后续研究,若哪位大佬有更好的见解,也请不吝赐教!