需求背景
需要一个输入框,可以输入文字,添加表情,一开始用了富文本编辑器,有点大材小用,所以自己封装一个输入框组件。支持输入文字,选择表情/插入表情,支持组合键换行,使用enter
进行提交
效果图
技术实现
- 通过原生
textarea
实现 - 通过
v-model
来实现 父子组件的数据传递,子组件监听数据的变化,向外emit('update:modelValue', inputValue)
,保证父组件能更新绑定的值 - 每次插入时,需要
重新聚焦
,更新光标位置
- 通过向外 暴露的 (__insertText),来实现
插入表情/文字
- 通过向外暴露的(_clear) 输入完毕发送后,需要
clear
掉输入框的内容 - 通过向外暴露的(__isEmpty) 来判断是否有内容,如果没内容,做按钮的
禁用状态
(当然也可以直接用父组件绑定的值) - 父组件通过
ref
就可以调用以上方法,来做操作。比如发送完数据,调clear清空内容
,如果输入框没有内容,则调用isEmpty
,做按钮的一些状态
代码实现(子组件)
/*** 自定义文本输入框组件*/import { ref, watch } from 'vue'
import { ElInput } from 'element-plus'export default defineComponent({props: {modelValue: {type: String,default: ''}},emits: ['update:modelValue', 'chatSend'],setup(props, { emit, expose }) {const { t } = useI18n()const editorRef = ref()const disabled = ref(false)const valueHtml = ref(props.modelValue)const currentEvent = ref()watch(() => valueHtml.value,() => {emit('update:modelValue', valueHtml.value)})// 插入元素const _insertText = async (msg: string) => {const msgLength = msg.lengthconst editor = currentEvent.valueif (editor) {const startPos = editor.selectionStartvalueHtml.value = `${valueHtml.value.substring(0, startPos)}${'' + msg}${valueHtml.value.substring(startPos)}`_focus()nextTick(() => {// 此处是 根据元素的长度,来设置光标位置editor.setSelectionRange(startPos + msgLength, startPos + msgLength)})}}// focusconst _focus = () => {editorRef.value && editorRef.value.focus()}// 清空编辑器const _clear = () => {editorRef.value && editorRef.value.clear()}// 是否内容为空const _isEmpty = () => {const str = valueHtml.value.trim().replace(/\n/g, '')return str === '' || str.length == 0 || typeof str === 'undefined'}// 禁用编辑器const _disable = () => {disabled.value = true}// 解除禁用编辑器const _enable = () => {disabled.value = false}const handleKeyDown = (event: KeyboardEvent) => {currentEvent.value = event.target as HTMLInputElement// 定义组合键 Mapconst shortCutKeys: (keyof KeyboardEvent)[] = ['metaKey', 'altKey', 'ctrlKey', 'shiftKey']const isEnterKey = event.code === 'Enter'const isShortcutKeys = shortCutKeys.some((item) => event[item])if (isEnterKey && isShortcutKeys) {// 获取光标位置const cursorPosition = currentEvent.value.selectionStart// 拆分成两段文本const textBeforeCursor = valueHtml.value.slice(0, cursorPosition)const textAfterCursor = valueHtml.value.slice(cursorPosition)// 合并为带有换行符的新文本const newText = textBeforeCursor + '\n' + textAfterCursor// 更新输入框的值valueHtml.value = newText// 文本编辑器的高度发生变化后nextTick(() => {// 高度变化 自动滚动到底部const editor = editorRef.value.textareaeditorRef.value.textarea.scrollTop = editor.scrollHeight// 设置光标位置为: start 和 end 相同,光标会移动到换行符后面的新行首currentEvent.value.setSelectionRange(cursorPosition + 1, cursorPosition + 1)})} else if (event.code === 'Enter') {// 阻止掉 Enter 的默认换行行为event.preventDefault()emit('chatSend')}}// 向外暴露方法expose({_insertText,_clear,_disable,_isEmpty,_enable,_focus})return () => (<ElInputref={editorRef}v-model={valueHtml.value}type="textarea"disabled={disabled.value}onKeydown={handleKeyDown}/>)}
})
代码实现(父组件调用)
输入框组件
<base-editorref="chatEditorRef"v-model="inputText"@chat-send="sendText"></base-editor>
工具栏组件
<ChatTools :chat-tools-list="newChatToolsList" @get-emoji="getToolsMsg" />
当我们点击工具栏组件,就会获取到工具栏的文字/表情/或者插入的xxx
,此时根据引用
,调用输入框的暴露出的_insertText
方法,直接就插入进去
const getToolsMsg = async (msg: string) => {chatEditorRef.value._insertText(msg)
}
其他方法调用
_isEmpty()
: 未输入,按钮是禁用状态
<el-button :disabled="chatEditorRef?._isEmpty()" @click="sendText">
当然你也可以直接使用绑定的输入框的变量去判断
<el-button :disabled="!inputText" @click="sendText">
_clear()
: 提交完请求,清空输入框
chatEditorRef.value._clear()
关于插入的问题
光标位置的获取,根据元素插入位置,光标换行,支持快捷键等。可以参考上一篇文章
https://blog.csdn.net/qq_38845858/article/details/135769222?spm=1001.2014.3001.5502
总结
- 原生其实也可以实现,没必要用一个
很重的富文本编辑器
- 做表情插入,或者其他插入,都是工具栏,和输入框组件的关系是
兄弟组件的关系,兄弟组件之间,怎么做数据传递,事件传递,怎么设计
?
(1) 父组件作为中介
(2)事件总线通过订阅/发布做消息通知
(3)仓库vuex/pina
事件总线还是能不用就不用,因为全局性的东西,用着爽,后面复杂了,就乱了。而且 事件总线是发布订阅,你还得注意销毁,存仓库 又不太合适,子组件自己暴露出方法,让其他组件调用
,感觉目前是最简单的方式了