Sender Input
A rich text input component based on tiptap, supporting various extensions and custom layouts.
Basic Usage
Extension Reminder
If you want to extend more HTML types, please refer to the source code and pass in extensions for customization.
Theme Settings
@Member Extension Usage
Extension Reminder
Uses the Mention extension provided by Tiptap.
./metion/suggestions file
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>Advanced Chat Input
- Set the maximum height of the input box via CSS.
- Combine with
ElADragUpload,useFileOperation, andElAFilesUploadto implement drag-and-drop, Ctrl+V pasting, and click-to-upload. - Use
ElAFilesCardto display uploaded files.
props
| Attribute | Description | Type | Default |
|---|---|---|---|
| v-model | HTML content of the input box | string | '' |
| v-model:show-input-tag-prefix | Whether to show the input box prefix tag | boolean | false |
| v-model:loading | Sending status; style can be customized via slot | boolean | false |
| theme | Theme | 'light' | 'dark' | 'light' |
| placeholder | Placeholder text | string | '' |
| disabled | Whether disabled | boolean | false |
| extensions | tiptap extension configuration | Array<Extensions> | [] |
| inputTagPrefixValue | Content of the input box prefix tag | string | '' |
| enterBreak | Whether Enter key inserts a line break. If false, Enter triggers the enterPressed event. | boolean | false |
| onHandleKeyDown | Custom keyboard event handling | (view: EditorView, event: KeyboardEvent) => void | - |
| variant | Layout variant | 'default' | 'updown' | 'default' |
slots
| Slot Name | Description | Scoped Arguments |
|---|---|---|
| prefix | Prefix content slot | - |
| input-tag-prefix | Custom content for input box prefix tag | - |
| action-list | Action bar list slot | - |
| send-btn | Send button slot | { disabled: boolean } |
| send-btn-loading | Displayed when loading is true | - |
| select-slot-content | Popup slot for select-slot clicks | { options: SenderSelectOption[] } |
events
| Event Name | Description | Callback Arguments |
|---|---|---|
| enterPressed | Triggered when Enter key is pressed (when enterBreak is false) | - |
| paste | Triggered on paste | (event: ClipboardEvent) |
| pasteFile | Triggered on file paste | (files: File[]) |
| blur | Triggered on blur | - |
| focus | Triggered on focus | - |
| send | Triggered when clicking the send button or pressing Enter to send | (content: string) |
exposes
| Name | Description | Type |
|---|---|---|
| editor | tiptap editor instance | () => Editor |
| focus | Auto focus | void |