成员列表
创建
实现成员列表的方式比较简单,其实就是一个列表,一个简单的v-for循环就可以搞定,点击时将当前选择的成员项回调给父组件。
新增一个AtPop.vue
文件:
<template> <div class="at-pop-index"> <div v-for="(item, index) in listData" :key="index" class="at-pop-item" @click.stop="clickItem(item)">{{item.name}}</div> </div> </template> <script> export default { props: { // 传入需要被@的成员数组 listData: { type: Array, default: () => [] } }, methods: { clickItem(item) { this.$emit('onSelect', item); // 调用父组件处理成员选择的方法,并回传当前选择项 } } } </script>
Copy
使用
在父组件中,注册并使用成员列表组件。我们需要在用户输入@的时候弹出成员列表,因此需要监听用户的输入,然后在用户选择成员后需要关闭。关键是获取光标位置,这个由输入框获取,在父组件只需要使用即可。
// 核心代码 ... // 选择成员时插入数据,并关闭弹窗 onSelect(item) { console.log('onSelect', item); this.$refs.inputBox.insertContent(`${item.name} `); // 有空格 this.isshowAt = false; }, // 输入框输入时回调函数 inputFunc(data, event) { console.log('inputFunc', data, event); if (event.data === '@') { this.isShowAt = true; // 显示弹窗 this.$nextTick(() => { let dom = document.getElementsByClassName('at-pop-index')[0]; // 获取成员列表弹窗,需要放在nextTick中 // 设置位置 dom.style.position = 'fixed'; dom.style.left = Math.floor(data.left + 10) + 'px'; dom.style.top = Math.floor(data.top) + 'px'; dom.style.zIndex = 9999; }) } else { this.isShowAt = false; } }, ...
Copy
输入框
输入框需要处理光标位置的获取、将值插入到光标的位置等,是本次功能实现的核心。
当输入框聚焦时,我们会看到光标闪动,想要获取光标的位置以便于插入数据,则需要借助Selection
对象。Selection
表示用户选择的一段文本范围或者插入数据的当前位置。既然是获取选取范围,那当前选择范围的index=0
就是当前光标的位置。我们想要实现的效果是成员列表跟随光标移动,因此就需要获取光标的坐标值。
获取光标的坐标
let range = window.getSelection().getRangeAt(0); // 获取当前光标 let position = range.getBoundinGClientRect(); // 获取当前光标的位置
Copy
getBoundingClientRect()
方法会返回一个DOMRect
矩形对象,其包含矩形区域的坐标值。将获取到的坐标值回调给父组件的方法,显示成员列表。
当我们在输入框输入@的时候,页面会出现成员列表,此时输入框还是聚焦的。但是如果我们点击了成员列表的某一项,此时输入框已经失焦了,虽然我们可以获取选择的成员并插入,如果只是简单的字符串追加的话,光标会在下次输入时默认定位到开头;或者我们需要在中间插入选择的成员,会发现没有位置可以插入。因此我们需要在失焦的时候先保存当前光标,并在插入时还原光标。
保存光标
// DivEditable.vue // 失焦 inputBlur(event) { this.selection = this.saveSelection(); this.$emit('blurFunc', event); }, // 失焦时保存光标 saveSelection() { if (!window.getSelection) { return null; } let sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { return sel.getRangeAt(0); } },
Copy
光标暂存了,我们需要将插入成员封装成方法,并可以给父组件调用,这样父组件在获取到成员信息后就可以直接调用。接下来需要使用上面已经保存的光标位置:
插入文本
// DivEditable.vue // 恢复光标 restoreSelection(range) { if (range) { let sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } }, // 插入数据 insertContent(value) { // this.$refs.editor.focus(); let range, node; this.restoreSelection(this.selection); // 还原失焦前的光标位置 range = window.getSelection().getRangeAt(0); range.collapse(false); // 光标移动到最后 node = range.createContextualFragment(value); let child = node.lastChild; console.log('lastChild', child); range.insertNode(node); // 将光标的起始位置设置在当前插入的元素后面 if (child) { range.setEndAfter(child); range.setStartAfter(child); } let sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); this.$emit('input', this.$refs.editor.innerhtml); },
Copy
其实到这里就基本实现了@功能,但是还有一个问题,当输入框失焦时回调了父组件的blurFunc
方法,导致成员列表关闭了,但是数据还没有拿到。处理这种问题,有两种思路:
- 使用setTimeout设置定时器,延后执行关闭成员列表操作
- 修改取数逻辑为异步操作,等到数据拿到后才关闭成员列表
简单点,就采用setTimeout实现。
// InputBox.vue blurFunc(event) { // 失焦时延时关闭弹窗,避免还未拿到数据 if (this.isShowAt) { setTimeout(() => { this.isShowAt = false; }, 500); } },
Copy
运行结果
输入框输入@,在光标位置附近弹出成员列表
选择成员后,将成员信息插入到输入框中
总结
本文介绍了实现输入框@功能的方法,简单易上手
主要使用了Selection
和Range
对象的相关方法,完成对光标的处理以及输入的插入
本文实现的@功能,无法删除时整体删除。目前有两种思路:将@xxx
转换为图片并插入到输入框,笔者简单写了demo验证了一下,效率不高,且会有卡顿;另一种方式就是监听退格键,删除时判断删除对象是否被包含着有意义的@xxx
中,如果是则整体删除,如果不是则默认的方式删除,这种方式笔者没有尝试,难度比较大。
来源|编程网 作者:泡泡鱼