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
指定在某种情况下,元素可以成为鼠标事件的目标。
- none,元素不会成为鼠标事件的目标,但是,不影响后代元素设置的值。
- 其他值,查阅文档 developer.mozilla.org/zh-CN/docs/… 。
安全性问题
- 用户在其中随意添加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焦点
anchorOffset,anchor点所在的Node节点为anchorNode,anchor点与Node节点元素标签的距离偏移量为anchorOffset(除去覆盖选中区域的那段偏移量)
focusOffset,同anchorOffset
禁止冒泡
在一些场景中,我们可能希望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事件的元素,那么,该元素的事件冒泡过程,都需要经过这一个不必要的判断逻辑。
另外,事件委托难以处理不冒泡的事件