likes
comments
collection
share

问个问题:如何实现一个受控的 input 框?

作者站长头像
站长
· 阅读数 29

前情提要


各位好!最近一直在学习一个 Markdown 编辑器的源码,本来想着看完再开始写文章,但由于工作比较忙,阅读源码也是断断续续的,所以就先写个开头出来,不然也好几个月没更新了。

富文本编辑器之前在工作中一直有使用到,也曾简单了解过它的实现原理,但并没有深入下去,直到这次恰好有个项目让我了解到为什么富文本编辑器是前端天花板(天坑)项目之一。

打个广告,这个 Markdown 编辑器是我老师的项目,欢迎各位点星🌟+收藏+关注,有问题可以评论留言,我会给老师进行反馈(但改不改就不是我说的算了🐶)

本文的demo地址:github.com/zhtzhtx/Ter…

实现 input 框


要实现一个不受控的 input 框非常简单,先定义一个 div,然后将它的样式中的 contenteditable 改成 true 就行了。

我们可以先定义 class,当它实例化的时候获取到目标DOM,并将其的 contenteditable 属性修改为true。

问个问题:如何实现一个受控的 input 框?

问个问题:如何实现一个受控的 input 框?

我们点击选中div,然后敲键盘就发现可以输入了,怎么样是不是非常简单?

问个问题:如何实现一个受控的 input 框?

控制英文输入


还记得我们的问题吗?我们要实现的是一个受控的 input 框,但现在用户想输什么就输什么的效果肯定不是我们所期望的,我们要对输入的内容进行控制,那么第一步当然禁止默认的输入事件。

我们可以通过监听 beforeinput 事件,然后在事件中调用 preventDefault 方法来禁止默认的输入事件。

问个问题:如何实现一个受控的 input 框?

然后当我们再次选择div并键盘输入时,发现div又没反映了,有人说:“啊,绕了一大圈,这和一开始的div有什么区别!”,别急,区别就在于我们在 beforeinput 事件中可以获取到用户输入了什么。

首先,我们可以通过 inputType 获取当前用户进行了什么操作,如果用户进行了键盘操作,那么 inputType 的值就为 insertText,接下来我们可以定义一个 insertText 方法来代替默认的输入方法。

问个问题:如何实现一个受控的 input 框?

在 insertText 方法中,我们可以通过事件对象的 data 属性来获取输入的值,然后通过 innerHTML 方法来将其显示在 div 中,这样当我们再次敲击键盘时候发现 div 中已经可以显示我们输入的值了。

问个问题:如何实现一个受控的 input 框?

但这时我们发现,虽然输入框中的文字虽然变化了但之前的输入的内容却消失了,这和我们预期的累积输入效果不同,所以我们需要缓存之前输入的内容。

在 constructor 中我们定义一个 spacers 字段用于缓存输入内容,在 insertText 函数中将新输入的内容添加到之前输入内容后面,并且显示在页面上。

问个问题:如何实现一个受控的 input 框?

这样当我们再次输入时 input 框中输入内容就能正常显示了,但是我们又发现了新的问题:为什么没有光标呢?而且当我们将光标插入到输入内容中间,再次输入内容时会发现新输入的内容还是添加到已输入内容后面的,而我们期望的是将其添加到光标位置。

所以接下来,我们要控制光标的位置和功能。

控制光标


首先我们需要知道光标是有两种状态的,线状和块状,块状光标就是选中了某一段文字,它有起点和终点,那线状就是起点和终点相同状态。

我们先在constructor 中我们定义 startIndex 和 endIndex 字段用于缓存光标的起点和终点,然后在每次输入之前,也就 insertText 函数中通过 window.getSelection 方法获取当前的光标的位置。

问个问题:如何实现一个受控的 input 框?

window.getSelection 方法返回的参数中有 anchorOffset 和 focusOffset 字段,它们代表着光标的起点和终点,需要注意的是,用户选中内容时可能是从前向后拖拽,也可能是从后向前拖拽( anchorOffset 可能小于 focusOffset,anchorOffset 也可能会大于 focusOffset) 我们需要自己判断起点和终点。

问个问题:如何实现一个受控的 input 框?

然后,this.spacers 就应该修改为当前文本内容的开头至光标起点 + 输入内容 + 光标终点至文本内容的结尾。

问个问题:如何实现一个受控的 input 框?

当文字输入完成时,光标会变成线状,同时光标应该在我们输入内容的末尾处,所以我们需要修改光标位置,这时 startIndex 应该加上输入内容的长度,而 endIndex 和位置一样。然后我们可以通过 document.createRange 方法来创建一个新的光标位置,setStart 和 setEnd 方法来设置光标的起点和终点,然后先清空当前页面的光标,再重新设置一个新的光标。

问个问题:如何实现一个受控的 input 框?

这样当我们再次输入内容时,会发现光标已经可以正常显示并且可以在文本中插入内容了。

控制删除


当我们完成文本输入后自然会想到如何删除输入文本呢,现在我们敲击 backspace 键会发现并没有效果,这是由于我们还未实现删除的逻辑。

和insertText一样,我们可以通过 inputType 是否为 deleteContentBackward 来判断当前是否触发删除事件。

删除的逻辑和插入一样,先获取当前光标的位置,判断当前光标是线状还是块状,如果是线状光标则将光标向前移动一位,然后删除光标后一位的内容,如果是块状光标,则将光标起点和终点之间的内容全部删除。

问个问题:如何实现一个受控的 input 框?

这样当我们再次尝试删除文本内容时,会发现文本内容已经成功删除了。

控制中文输入


如果我们在 input 框中输入中文时,会发现中文是可以正常输入的,但当我们尝试输入英文或者删除会发现我输入的中文不见了,这由于中文输入比较特殊,它不在 beforeInput 当中监听,所以我们的 this.spacers、this.startIndex 和 this.endIndex 并没有更新数据,导致显示错误。

中文输入的监听是使用 compositionend 事件,和英文输入相同,当中文输入完成后,我们需要将输入文本添加到 this.spacers 中,同时更新光标位置。

问个问题:如何实现一个受控的 input 框?

问个问题:如何实现一个受控的 input 框?

相信各位也看出来了,中文输入我们并没有控制显示内容和光标信息,这是由于 compositionend 事件的调用时机是中文输入已经完成时,也就是说 innerHTML 和 光标位置已经发生改变了,同时这个事件中我们不能使用 preventDefault 方法来拦截默认事件。

目前看起来中文输入并不能完全受控,如果你有好的想法可以在评论区留言。

结语


好了,这样一个受控的 input 框就完成了,是不是听上去简单做起来还挺复杂的呢,当然我们的工作才刚刚开始,像撤销、返回等功能都还没实现呢,这部分我们下期再讲!

转载自:https://juejin.cn/post/7213955045242306618
评论
请登录