Sender 输入框
基于 tiptap 的富文本输入框组件,支持多种扩展和自定义布局。
基础用法
扩展提醒
如果你想扩展更多html类型,可以查看源码方式,自行传入extensions进行扩展使用
主题设置
@成员 扩展用法
扩展提醒
使用tiptap提供的Mention扩展能力。
./metion/suggestions 文件
ts
import { computePosition, flip, shift } from '@floating-ui/dom'
import type { VirtualElement } from '@floating-ui/dom'
import { posToDOMRect, VueRenderer } from '@tiptap/vue-3'
import { Editor } from '@tiptap/core'
import type { SuggestionProps } from '@tiptap/suggestion'
import MentionList from './MentionList.vue'
const updatePosition = (editor: Editor, element: HTMLElement) => {
const virtualElement: VirtualElement = {
getBoundingClientRect: () =>
posToDOMRect(
editor.view,
editor.state.selection.from,
editor.state.selection.to
),
}
computePosition(virtualElement, element, {
placement: 'bottom-start',
strategy: 'absolute',
middleware: [shift(), flip()],
}).then(({ x, y, strategy }) => {
element.style.width = 'max-content'
element.style.position = strategy
element.style.left = `${x}px`
element.style.top = `${y}px`
})
}
const suggestions: any[] = [
{
char: '@',
items: ({ query }: { query: string }) => {
return [
'Lea Thompson',
'Cyndi Lauper',
'Tom Cruise',
'Madonna',
'Jerry Hall',
'Joan Collins',
'Winona Ryder',
'Christina Applegate',
'Alyssa Milano',
'Molly Ringwald',
'Ally Sheedy',
'Debbie Harry',
'Olivia Newton-John',
'Elton John',
'Michael J. Fox',
'Axl Rose',
'Emilio Estevez',
'Ralph Macchio',
'Rob Lowe',
'Jennifer Grey',
'Mickey Rourke',
'John Cusack',
'Matthew Broderick',
'Justine Bateman',
'Lisa Bonet',
]
.filter((item) => item.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 5)
},
render: () => {
let component: VueRenderer
return {
onStart: (props: SuggestionProps) => {
component = new VueRenderer(MentionList, {
// using vue 2:
// parent: this,
// propsData: props,
// using vue 3:
props,
editor: props.editor,
})
if (!props.clientRect) {
return
}
;(component.element as HTMLElement).style.position = 'absolute'
document.body.appendChild(component.element as HTMLElement)
updatePosition(props.editor, component.element as HTMLElement)
},
onUpdate(props: SuggestionProps) {
component.updateProps(props)
if (!props.clientRect) {
return
}
updatePosition(props.editor, component.element as HTMLElement)
},
onKeyDown(props: SuggestionProps) {
// @ts-ignore
if (props.event.key === 'Escape') {
component.destroy()
return true
}
return component.ref?.onKeyDown(props)
},
onExit() {
component.destroy()
},
}
},
},
{
char: '#',
items: ({ query }: { query: string }) => {
return ['Dirty Dancing', 'Pirates of the Caribbean', 'The Matrix']
.filter((item) => item.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 5)
},
render: () => {
let component: VueRenderer
return {
onStart: (props: SuggestionProps) => {
component = new VueRenderer(MentionList, {
// using vue 2:
// parent: this,
// propsData: props,
// using vue 3:
props,
editor: props.editor,
})
if (!props.clientRect) {
return
}
;(component.element as HTMLElement).style.position = 'absolute'
document.body.appendChild(component.element as HTMLElement)
updatePosition(props.editor, component.element as HTMLElement)
},
onUpdate(props: SuggestionProps) {
component.updateProps(props)
if (!props.clientRect) {
return
}
updatePosition(props.editor, component.element as HTMLElement)
},
onKeyDown(props: SuggestionProps) {
// @ts-ignore
if (props.event.key === 'Escape') {
component.destroy()
return true
}
return component.ref?.onKeyDown(props)
},
onExit() {
component.destroy()
},
}
},
},
]
export default suggestionsvue
<template>
<div class="dropdown-menu">
<template v-if="items.length">
<button
:class="{ 'is-selected': index === selectedIndex }"
v-for="(item, index) in items"
:key="index"
@click="selectItem(index)"
>
{{ item }}
</button>
</template>
<div class="item" v-else>No result</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
required: true,
},
command: {
type: Function,
required: true,
},
},
data() {
return {
selectedIndex: 0,
}
},
watch: {
items() {
this.selectedIndex = 0
},
},
methods: {
onKeyDown({ event }) {
if (event.key === 'ArrowUp') {
this.upHandler()
return true
}
if (event.key === 'ArrowDown') {
this.downHandler()
return true
}
if (event.key === 'Enter') {
this.enterHandler()
return true
}
return false
},
upHandler() {
this.selectedIndex =
(this.selectedIndex + this.items.length - 1) % this.items.length
},
downHandler() {
this.selectedIndex = (this.selectedIndex + 1) % this.items.length
},
enterHandler() {
this.selectItem(this.selectedIndex)
},
selectItem(index) {
const item = this.items[index]
if (item) {
this.command({ id: item })
}
},
},
}
</script>
<style scoped lang="scss">
/* Dropdown menu */
.dropdown-menu {
background: #fff;
border: 1px solid rgba(61, 37, 20, 0.05);
border-radius: 0.7rem;
box-shadow:
0px 12px 33px 0px rgba(0, 0, 0, 0.06),
0px 3.618px 9.949px 0px rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
gap: 0.1rem;
overflow: auto;
padding: 0.4rem;
position: relative;
button {
align-items: center;
background-color: transparent;
display: flex;
gap: 0.25rem;
text-align: left;
width: 100%;
padding: 0 5px;
border-radius: 4px;
color: black;
&:hover,
&:hover.is-selected {
background-color: rgba(61, 37, 20, 0.12);
}
&.is-selected {
background-color: rgba(61, 37, 20, 0.08);
}
}
}
</style>高级聊天输入框
- 通过 CSS 设置输入框最大高度。
- 搭配
ElADragUpload、useFileOperation、ElAFilesUpload实现拖拽、Ctrl+V 粘贴、点击上传文件。 - 使用
ElAFilesCard实现文件上传回显。
props
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| v-model | 输入框的html | string | '' |
| v-model:show-input-tag-prefix | 是否显示输入框前置标签 | boolean | false |
| v-model:loading | 发送中、可以通过slot自定义样式 | boolean | false |
| theme | 主题 | 'light' | 'dark' | 'light' |
| placeholder | 占位文本 | string | '' |
| disabled | 是否禁用 | boolean | false |
| extensions | tiptap 扩展配置 | Array<Extensions> | [] |
| inputTagPrefixValue | 输入框前置标签内容 | string | '' |
| enterBreak | 回车是否换行,为 false 时回车触发 enterPressed 事件 | boolean | false |
| onHandleKeyDown | 自定义键盘事件处理 | (view: EditorView, event: KeyboardEvent) => void | - |
| variant | 布局变体 | 'default' | 'updown' | 'default' |
slots
| 插槽名 | 说明 | 作用域参数 |
|---|---|---|
| prefix | 前置内容插槽 | - |
| input-tag-prefix | 输入框前置标签自定义内容 | - |
| action-list | 操作栏列表插槽 | - |
| send-btn | 发送按钮插槽 | { disabled: boolean } |
| send-btn-loading | loading为true的时候,显示loading按钮 | - |
| select-slot-content | select-slot 点击弹窗插槽 | { options: SenderSelectOption[] } |
events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| enterPressed | 回车键按下时触发(当 enterBreak 为 false) | - |
| paste | 粘贴时触发 | (event: ClipboardEvent) |
| pasteFile | 粘贴文件时触发 | (files: File[]) |
| blur | 失去焦点时触发 | - |
| focus | 获得焦点时触发 | - |
| send | 点击发送按钮或回车发送时触发 | (content: string) |
exposes
| 名称 | 说明 | 类型 |
|---|---|---|
| editor | tiptap editor 实例 | () => Editor |
| focus | 自动聚焦 | void |