使用contenteditable模拟文本框

568 阅读6分钟

contenteditable定义

  • 标记元素是否可以被编辑。
  • true或空字符串,元素可编辑;false,元素不可编辑;plaintext-only,元素的原始文本可编辑,但禁用富文本格式。

常用元素

  • a,设置href,但点击不会触发跳转事件。
  • button,可编辑内部的文本,也可触发点击事件。
  • div、p、span、input,正常使用编辑功能。

兼容性问题

  • 不同的浏览器对contenteditable的实现方式可能是不一致的,可能会导致相同的代码在不同的浏览器中,表现出不一致的效果和行为。
  • IE8模式下,元素设置contenteditable为false,可能会不显示焦点。
  • IE8及以下版本的浏览器,contenteditable为true的元素包裹contenteditable为false的元素,可能会导致false的元素仍然可以编辑。

解决思路

  • 注意查阅文档的兼容性。
  • 手动设置焦点的显示。
  • 将内部为false的元素,设置pointer-events为none,或js设置preventDefault,阻止默认的编辑操作。

pointer-events

指定在某种情况下,元素可以成为鼠标事件的目标。

安全性问题

  • 用户在其中随意添加HTML内容,发送至服务器时可能会存在XSS等攻击的风险。

解决思路

  • 使用plaintext-only值,禁用富文本格式。
  • 对输入的内容进行过滤以及转义。
  • 使用innerHTML获取contenteditable为true的内容,会进行转义处理。

功能模拟

使用contenteditable为true的元素替代编辑器时,为了实现更多的功能,需要做出一些额外的处理。

<div contenteditable="true" class="input-box"></div>
const inputEl = document.getElementsByClassName('input-box')[0]

监听文本变化

inputEl.addEventListener('input', action)

绑定一个input事件即可

设置最多字数

inputEl.addEventListener('input', handleChange)
const maxLength = 100
const handleChange = (e) => {
    const text = e.target.innerText
    if (!e.isComposing) {
        if (text.length > maxLength) {
            inputEl.innerText = text.slice(0, maxLength)
        }
    }
}
  • isComposing,用于判断当前是否为中文输入状态,若是,则不做字数限制,否则,拼音可能无法继续输入。
  • 若是英文输入,则在超限时对长度进行裁切。

但这样子会有一个问题,中文输入最后一段文字且超长时,超长文字不会被处理。尝试了两种解决办法

composition相关的监听器
  • compositionstart,用户开始输入拼音时触发。
  • compositionupdate,用户输入拼音时、选择文字时触发。
  • compositionend,用户完成输入时触发。

需要注意输入汉字过程的触发顺序

  • 按下第一个拼音字母时,先触发compositionstart,再触发compositionupdate,再触发input。
  • 后续每按下一个拼音字母,先触发compositionupdate,再触发input。
  • 最后选择拼音对应的文本时,先触发compositionupdate,再触发input,再触发compositionend。

因此,可以在compositionupdate或compositionend中加入处理,具体选择取决于input对该处理的逻辑影响

blur监听器
  • 使用blur监听器,可以在用户完成输入后,失去焦点时再将字符进行裁剪。
  • 但做不到及时处理。

设置焦点

如果进行了最大字数的限制,那么,在进行文字裁剪时,重新为元素赋值文本后,焦点会显示在头部,需要手动进行位置的恢复

const El = document.getElementsByClassName("***")
const selection = window.getSelection()
let anchorOffset = selection.anchorOffset
const range = document.createRange()
range.setStart(El.childNodes[0], anchorOffset)
range.collapse(true)
selection.removeAllRanges()
selection.addRange(range)
El.focus()
  • anchorOffset为光标需要的偏移量,具体取值规则根据场景不同而需要更改。
Selection

记录光标选中的文本位置信息、光标当前的位置信息 鼠标滑动选中文本时,按下鼠标的位置记录为anchor锚点,松开鼠标的位置记录为focus焦点

1.png

anchorOffset,anchor点所在的Node节点为anchorNode,anchor点与Node节点元素标签的距离偏移量为anchorOffset(除去覆盖选中区域的那段偏移量)

focusOffset,同anchorOffset

2.png

禁止冒泡

在一些场景中,我们可能希望contenteditable为true的元素,在进行一些键盘、鼠标等操作时,其事件不会被外层的元素捕获 如,在输入类组件中内嵌contenteditable为true的元素,在编辑器中内嵌contenteditable为true的元素

const cancelBubble = (e) => {
    if (e && e.stopPropagation) {
        e.stopPropagation()
    } else if (window && window.event) {
        window.event.cancelBubble = true
    }
}
  • e接收的是event事件。
  • stopPropagation可以阻止事件向外层元素冒泡。
  • 事件冒泡,一个元素上的事件被触发后,事件会从当前元素沿着DOM树向上冒泡到外层元素,直至根节点,即它的外层元素也会接收到该事件,且可以对该事件进行处理、响应。
  • 还有一个阻止冒泡的方法,preventDefault,阻止事件的默认行为。
inputEl.addEventListener('keydown', cancelBubble)
inputEl.addEventListener('keyup', cancelBubble)
inputEl.addEventListener('input', cancelBubble)
inputEl.addEventListener('blur', cancelBubble)
inputEl.addEventListener('copy', cancelBubble)
inputEl.addEventListener('paste', cancelBubble)
inputEl.addEventListener('cut', cancelBubble)
  • 如果contenteditable为true的元素外层有可编辑元素,可以对以上的一些事件进行禁止冒泡。
  • 当然,不同的事件,可以替换为不同的处理方法。

事件委托

理同事件冒泡,事件在子元素上触发后,会冒泡到父元素,父元素可以对该事件做处理 如果我们的场景中,需要展示多个contenteditable为true的元素,对每个元素都注册相同的事件,则会出现许多重复的代码逻辑,占用较多的内存 我们可以选择它们的一个外层元素,将它们的事件都委托给该元素


<div class="box">
    <div class="input" contenteditable="true"></div>
    <div class="input" contenteditable="true"></div>
    <div class="input" contenteditable="true"></div>
</div>

不使用事件委托,需要设置三个监听器

const Els = document.getElementsByClassName('input')
for (let el of Els) {
    el.addEventListener('change', action)
}

使用事件委托,需要设置一个监听器

const Els = document.getElementsByClassName('box')
Els[0].addEventListener('change', action)
  • 处理触发事件时,可通过action方法接收的event事件,在其中查看发送事件的元素信息。
  • 通过发送事件元素信息的类名等信息,可以判断哪些事件需要进行指定的处理的。

在使用事件委托时,需要注意当前的DOM嵌套结构的深度,否则,事件冒泡过多的外层元素,可能会造成一些问题

  • 可能被某一层阻止冒泡了,导致指定委托元素无法处理。
  • 可能造成不必要的函数处理。如前面所言,被委托的元素,需要判断接收的事件是否需要处理。而如果该元素有同样会冒泡change事件的元素,那么,该元素的事件冒泡过程,都需要经过这一个不必要的判断逻辑。

另外,事件委托难以处理不冒泡的事件