文本编辑的相关知识工作中最近做了一个关于文本编辑的相关需求。踩了一些坑,特此记录。需求阐述:这里就是一个传统的输入框。不过要计算你触发参数框的位置。也就是说记录你光标的位置。参数框出现之后,要支持一些键盘交互。这里利用原生事件绑定一下就可以了。选中相关的参数将其应用到输入框中。
- 利用contenteditable元素来模拟输入框。利用这个来模拟有优点也有缺点,优点是div可以自适应高度。缺点是复制HTML的内容可以直接复制进去,并且当你利用js控制输入的内容。也就是说你当手动控制输入的内容时,光标会跳到开头。
第一个问题html的内容可以直接复制进去。contenteditable的属性值有很多。
1 2 3 4 5 6
| contenteditable="" contenteditable="events" contenteditable="caret" contenteditable="plaintext-only" contenteditable="true" contenteditable="false"
|
其中的plaintext-only就是让元素只能键入纯文本。true是也可以输入html元素。其它的属性我并没有去弄清楚,感兴趣的可以自己查阅相关的资料。
contenteditable元素的placeholder可以利用css来控制(edit是元素的classname), 在元素上利用pleaceholder属性来写上你要的placeholder的内容。
1 2 3 4 5 6 7
| .edit:empty::before { content: attr(placeholder); }
.edit:focus:before { content: none; }
|
让元素变得可编辑其实也可以利用css来控制,user-modify。这里可以参考张鑫旭写的文章:https://www.zhangxinxu.com/wordpress/2016/01/contenteditable-plaintext-only
2. 记录光标的位置。
通过分析需求知道这里的参数框应该是脱离文档流。那么利用绝对定位或者固定定位都可以, 我用的绝对定位,所以我们需要计算光标的位置。
在js中,万物皆对象。由此可知。光标当然也是。于是我在MDN查了一下。发现我们Selection对象。MDN中相关知识点
它表示用户选择的文本范围或光标当前位置,也就是我们可以文字变蓝色的那个区域。而光标对象就在Selection中。可以通过getRangeAt方法获取.MDN中相关知识点.并且还可以通过Document对象构造Range(这个之后会用到).
有了rang对象,通过getBoundingClientRect方法可以得到与该对象相关的css边框集合(也就是DOMRect对象)。DOMRect 对象包含了一组用于描述边框的只读属性——left、top、right和bottom,单位为像素。除了 width 和 height 外的属性都是相对于视口的左上角位置而言的。
1 2 3 4 5 6 7
| .edit:empty::before { content: attr(placeholder); }
.edit:focus:before { content: none; }
|
然后将left和top的值赋给参数框就可以了。
注: 这节的相关知识常用的场景是富文本编辑框。但这里面的坑也是数不胜数。感兴趣的可以看看这篇文章(https://imys.net/20161125/cursor-offset-at-input.html)
3. 每次手动改变输入框的内容时,光标位置总会变到最前面。
当参数框出现时我需要一些键盘交互。这里我没有利用框架的UI,自己手动注册的事件。还挺简单的。
这个面板的html代码:
1 2 3 4 5 6 7 8 9 10 11 12
| <el-card ref="paramPanel" v-show="paramPanel.cascaderPanelIsShow" class="cascader-panel" v-bind:style="paramPanel.styleObject"> <div v-if="params.length" ref="cascaderContent" class="cascader-content"> <div class="cascader-item" v-for="(param,index) in params" :key="index" :ref="'paramMenu'+index" :class="index == currentIndex?'isActive':''" @click="() => getCurrentParam(param.label)"> {{param.label}} </div> </div> <div class="no-data-panel" v-else>NO DATA</div> </el-card>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| if (e.code === 'ArrowUp') { e.preventDefault(); if (this.currentIndex === 0) { this.currentIndex = this.params.length - 1; } else { this.currentIndex--; } this.scrollParamPanelBar() } else if (e.code === 'ArrowDown') { e.preventDefault(); if (this.currentIndex === this.params.length - 1) { this.currentIndex = 0; } else { this.currentIndex++; } this.scrollParamPanelBar() } else if (e.code === 'Escape') { this.makeParamPanelDisappear() } else if (e.code === 'Enter') { e.preventDefault(); if (this.params[this.currentIndex] && this.params[this.currentIndex].label) { this.makeParamPanelDisappear() }
|
判断是否滚动条的方法: scrollParamPanelBar
1 2 3 4 5 6 7 8
| let offsetTop = this.$refs['paramMenu' + (this.currentIndex)][0].offsetTop
if (offsetTop + this.$refs['paramMenu' + (this.currentIndex)][0].clientHeight > this.$refs['paramPanel'].$el.clientHeight) { this.$refs['paramPanel'].$el.scrollTop = offsetTop + 当前选中的元素的高度(clientHeight) - this.$refs['paramPanel'].$el.clientHeight;
} else if (this.$refs.cascaderContent.scrollHeight == this.$refs.cascaderContent.scrollTop + this.$refs.cascaderContent.clientHeight) { this.$refs['paramPanel'].$el.scrollTop = 0; }
|
因为参数被应用到输入框中,也就是说手动改了输入框的内容。那么光标对象会被重置到内容的最前面。产生这样的现象的原因是因为当你用js改变输入框的内容时并没有去创建光标对象,所以在这里就用到了上面的知识点利用Document对象构造Range,并手动控制光标的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| let selection = document.getSelection();
let range = document.createRange()
let newValue = document.createTextNode(editValue)) range.selectNodeContents(newValue)
range.setStart(newValue, index)
range.collapse(true)
selection.removeAllRanges()
selection.addRange(range)
|
这里其实主要运用的就是range的相关知识点MDN中相关知识点,你的需求可能不同但只要明白range相关api.调用就可以了。