利用contentEditable=”true”做多行文本输入框

需求

当textArea或者已有的富文本编辑器不能满足业务需求时,需要特殊定制该业务的多行文本编辑器。如:左边显示文本行数,右边分行编辑对应数据

image.png

分析

数据是一串数组,要把数组中的内容按照如上图所示展示,同时像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的值判断点击的哪个按键并处理数据和光标。
点击某一行文本时,记录当前点击时光标所在位置(为后续键盘处理上下按键时所需要的)
比如:当我点击到第四行的‘先’和‘说’中间时
image.png
再去按两次键盘‘下’方向键时,若该行文字长度小于光标所在位置,则光标处于最末尾,若该行文本内容大于所记录的光标位置,则光标处于光标所记录的位置。
image.png
image.png

  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。
image.png

其他

该案例的处理还有很多不到位的地方,比如对浏览器兼容的处理,后续有时间再做更改。
看有些富文本编辑器的光标采用自定义光标的方式,比如掘金写文章这里。这些等待后续研究,若哪位大佬有更好的见解,也请不吝赐教!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享