文章说明

我有使用多个提及符的使用场景,并且每一种提及符对应的数据集以及 UI 均不同,所以就找到了以下解决方案。

首先,我们需要按照 Tiptap 官方文档,安装 Tiptap ,我这里使用的是 Vue3,对其他前端框架一样起作用,只是相应的语法可能有些改变。

实现方式

Tiptap 安装

1
npm install @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit @tiptap/extension-mention

创建 Editor 组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<editor-content :editor="editor" />
</template>

<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'

const editor = useEditor({
content: '<p>I’m running Tiptap with Vue.js. 🎉</p>',
extensions: [
StarterKit,
],
})
</script>

在你的页面引入组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div id="app">
<tiptap />
</div>
</template>

<script>
import Tiptap from './components/Tiptap.vue'

export default {
name: 'App',
components: {
Tiptap
}
}
</script>

引入 Mention 组件相关依赖

1
2
3
npm install @tiptap/extension-mention
npm install tippy.js
npm install @tiptap/suggestion

创建 Mention 提及组件的 UI 组件 MentionList.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<template>
<div class="items">
<template v-if="items.length">
<button
class="item"
: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 lang="scss">
.items {
padding: 0.2rem;
position: relative;
border-radius: 0.5rem;
background: #FFF;
color: rgba(0, 0, 0, 0.8);
overflow: hidden;
font-size: 0.9rem;
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.05),
0px 10px 20px rgba(0, 0, 0, 0.1),
;
}

.item {
display: block;
margin: 0;
width: 100%;
text-align: left;
background: transparent;
border-radius: 0.4rem;
border: 1px solid transparent;
padding: 0.2rem 0.4rem;

&.is-selected {
border-color: #000;
}
}
</style>

创建 Mention 提及组件的数据处理组件 suggestion.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import { VueRenderer } from '@tiptap/vue-3'
import tippy from 'tippy.js'

import MentionList from './MentionList.vue'

export default {
items: ({ query }) => {
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
let popup

return {
onStart: props => {
component = new VueRenderer(MentionList, {
// using vue 2:
// parent: this,
// propsData: props,
// using vue 3:
props,
editor: props.editor,
})

if (!props.clientRect) {
return
}

popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
})
},

onUpdate(props) {
component.updateProps(props)

if (!props.clientRect) {
return
}

popup[0].setProps({
getReferenceClientRect: props.clientRect,
})
},

onKeyDown(props) {
if (props.event.key === 'Escape') {
popup[0].hide()

return true
}

return component.ref?.onKeyDown(props)
},

onExit() {
popup[0].destroy()
component.destroy()
},
}
},
}

在你的页面引入上述内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import Mention from '@tiptap/extension-mention'
import suggestion1 from '@/plugins/suggestion'
import suggestion2 from '@/plugins/suggestion2'

const editor = useEditor({
extensions: [
StarterKit.configure({
heading: false
}),
Image,
Underline,
Mention
.extend({
name: 'customMentionOne',
})
.configure({
HTMLAttributes: {
class: "mention"
},
suggestion: suggestion1
}),
Mention
.extend({
name: 'customMentionTwo',
})
.configure({
HTMLAttributes: {
class: "mention"
},
suggestion: suggestion2
})
],
content: props.modelValue,
onUpdate: ({ editor }) => {
emit('update:modelValue', editor.getHTML())
},
})

实现重点

实现 Mention 提及组件同时添加多个实例的方法就是,给每个 Mention 提及组件指定一个唯一的名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Mention
.extend({
name: 'customMentionOne',
})
.configure({
HTMLAttributes: {
class: "mention"
},
suggestion: suggestion1
}),
Mention
.extend({
name: 'customMentionTwo',
})
.configure({
HTMLAttributes: {
class: "mention"
},
suggestion: suggestion2
})

如上述代码所示,在 Mention.extend 中设置了 Mention 实例的 name 属性,如果你要创建多个,将他们的 name 属性区分好就可以了。

除了需要修改 name 属性以外,还需要修改的一处地方是 suggestion.js 文件中的 pluginKey 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import tippy from 'tippy.js'
import { PluginKey } from '@tiptap/pm/state'
// suggestion1.js
export default {
char: '@',
pluginKey: new PluginKey("suggestionOne"),
// ...
}
// suggestion2.js
export default {
char: '#',
pluginKey: new PluginKey("suggestionTwo"),
// ...
}

如上述代码所示,PluginKey 同样需要保持不同,这样就实现了 Tiptap 的提及组件 Mention 同时添加多个实例。