实现一个简单的输入框
需求背景
需要一个输入框,可以输入文字,添加表情,一开始用了富文本编辑器,有点大材小用,所以自己封装一个输入框组件。支持输入文字,选择表情/插入表情,支持组合键换行,使用enter 进行提交
效果图
技术实现
- 通过原生
textarea
实现 - 通过
v-model
来实现 父子组件的数据传递,子组件监听数据的变化,向外emit('update:modelValue', inputValue)
,保证父组件能更新绑定的值 - 每次插入时,需要
重新聚焦
,更新光标位置
- 通过向外 暴露的 (__insertText),来实现
插入表情/文字
- 通过向外暴露的(_clear) 输入完毕发送后,需要
clear
掉输入框的内容 - 通过向外暴露的(__isEmpty) 来判断是否有内容,如果没内容,做按钮的
禁用状态
(当然也可以直接用父组件绑定的值) - 父组件通过
ref
就可以调用以上方法,来做操作。比如发送完数据,调clear清空内容
,如果输入框没有内容,则调用isEmpty
,做按钮的一些状态
代码实现(子组件)
js
/**
* 自定义文本输入框组件
*/
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.length
const editor = currentEvent.value
if (editor) {
const startPos = editor.selectionStart
valueHtml.value = `${valueHtml.value.substring(0, startPos)}${'' + msg}${valueHtml.value.substring(startPos)}`
_focus()
nextTick(() => {
// 此处是 根据元素的长度,来设置光标位置
editor.setSelectionRange(startPos + msgLength, startPos + msgLength)
})
}
}
// focus
const _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
// 定义组合键 Map
const 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.textarea
editorRef.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 () => (
<div class="chatEditor">
<ElInput
ref={editorRef}
v-model={valueHtml.value}
type="textarea"
disabled={disabled.value}
onKeydown={handleKeyDown}
/>
</div>
)
}
})
代码实现(父组件调用)
输入框组件
js
<base-editor
ref="chatEditorRef"
v-model="inputText"
@chat-send="sendText"
></base-editor>
工具栏组件
js
<ChatTools :chat-tools-list="newChatToolsList" @get-emoji="getToolsMsg" />
当我们点击工具栏组件,就会获取到工具栏的文字/表情/或者插入的xxx
,此时根据引用
,调用输入框的暴露出的_insertText
方法,直接就插入进去
js
const getToolsMsg = async (msg: string) => {
chatEditorRef.value._insertText(msg)
}
其他方法调用
_isEmpty()
: 未输入,按钮是禁用状态
js
<el-button :disabled="chatEditorRef?._isEmpty()" type="primary" @click="sendText">
当然你也可以直接使用绑定的输入框的变量去判断
js
<el-button :disabled="!inputText" @click="sendText">
_clear()
: 提交完请求,清空输入框
js
chatEditorRef.value._clear()
关于插入的问题
光标位置的获取,根据元素插入位置,光标换行,支持快捷键等。可以参考上一篇文章 juejin.cn/post/732680...
总结
- 原生其实也可以实现,没必要用一个很重的富文本编辑器
- 做表情插入,或者其他插入,都是工具栏,和输入框组件的关系是
兄弟组件的关系,兄弟组件之间,怎么做数据传递,事件传递,怎么设计
?
(1) 父组件作为中介
(2)事件总线通过订阅/发布做消息通知
(3)仓库vuex/pina
事件总线还是能不用就不用,因为全局性的东西,用着爽,后面复杂了,就乱了。而且 事件总线是发布订阅,你还得注意销毁,存仓库 又不太合适,子组件自己暴露出方法,让其他组件调用
,感觉目前是最简单的方式了