import { mergeAttributes } from '@tiptap/core'
import { Mention as MentionTipTap } from '@tiptap/extension-mention'
import { Plugin, PluginKey } from 'prosemirror-state'
export function mentionPastePlugin (options) {
const { suggestion, idKey = 'memberId', labelKey = 'displayName' } = options
const cache = new Map()
const getMentionItems = suggestion.items
let editorViewRef = null
return new Plugin({
key: new PluginKey('mentionPaste'),
view (view) {
editorViewRef = view
return {
destroy () {
editorViewRef = null
}
}
},
appendTransaction (transactions, _, newState) {
const isPaste = transactions.some(tr => tr.getMeta('paste'))
if (!isPaste) return null
const tr = newState.tr
let modified = false
const promises = []
newState.doc.descendants((node, pos) => {
if (!node.isText) return
const getItem = async (query, start, end) => {
let items = cache.get(query)
if (!items) {
items = (await getMentionItems({ query })) || []
items = items
.map(item => ({ ...item, mention: `@${item[labelKey]}` }))
.sort((a, b) => b[labelKey].length - a[labelKey].length)
cache.set(query, items)
}
return items.find(item =>
node.text.slice(start, end) === item.mention
)
}
const matches = [...node.text.matchAll(/@[\w]+/g)]
if (!matches.length) return
for (const match of matches) {
const matchText = match[0]
const start = match.index ?? 0
const end = start + matchText.length
const query = matchText.slice(1)
promises.push(
(async () => {
const item = await getItem(query, start, end)
if (!item) return
const from = pos + start
const to = pos + end
const mentionNode = newState.schema.nodes.mention.create({
id: item[idKey],
label: item[labelKey]
})
tr.replaceWith(from, to, mentionNode)
modified = true
})()
)
}
})
if (promises.length) {
Promise.all(promises)
.then(() => {
if (modified) {
if (editorViewRef) editorViewRef.dispatch(tr)
}
cache.clear()
})
.catch(console.error)
}
return null
}
})
}
export const Mention = MentionTipTap.extend({
addOptions () {
const parent = this.parent?.()
return {
...parent,
renderHTML ({ options, node }) {
const isSelf = node.attrs.id === options.myMentionId
return [
'span',
{
...mergeAttributes(this.HTMLAttributes, options.HTMLAttributes),
class: `mention ${isSelf ? 'mention-me' : ''}`
},
`${options.suggestion.char || ''}${node.attrs.label ?? node.attrs.id}`
]
},
suggestion: {
...parent?.suggestion,
startOfLine: false,
allowedPrefixes: [
' ',
// Chinese, Japanese, and Korean
'\\u2E80-\\u2EFF\\u2F00-\\u2FDF\\u3040-\\u30FF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\u9FFF\\uF900-\\uFAFF\\uAC00-\\uD7AF\\u1100-\\u11FF\\u3130-\\u318F'
]
},
myMentionId: null
}
},
addStorage () {
return { suggestionOpen: false }
},
addAttributes () {
return {
...this.parent?.(),
class: { default: 'mention' }
}
},
renderLabel ({ options, node }) {
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
},
addProseMirrorPlugins () {
return [...(this.parent?.() || []), mentionPastePlugin(this.options)]
}
})