Add custom FilterMention (no @)
This commit is contained in:
parent
d117984585
commit
1d4f1b6211
1 changed files with 179 additions and 0 deletions
179
extensions/FilterMention/index.tsx
Normal file
179
extensions/FilterMention/index.tsx
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
import { mergeAttributes, Node } from '@tiptap/core'
|
||||||
|
import {
|
||||||
|
CustomSuggestion,
|
||||||
|
SuggestionOptions,
|
||||||
|
} from '~extensions/CustomSuggestion'
|
||||||
|
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||||
|
import { PluginKey } from '@tiptap/pm/state'
|
||||||
|
|
||||||
|
export type MentionOptions = {
|
||||||
|
HTMLAttributes: Record<string, any>
|
||||||
|
renderLabel: (props: {
|
||||||
|
options: MentionOptions
|
||||||
|
node: ProseMirrorNode
|
||||||
|
}) => string
|
||||||
|
suggestion: Omit<SuggestionOptions, 'editor'>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MentionPluginKey = new PluginKey('mention')
|
||||||
|
|
||||||
|
export const FilterMention = Node.create<MentionOptions>({
|
||||||
|
name: 'mention',
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {},
|
||||||
|
renderLabel({ options, node }) {
|
||||||
|
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
|
||||||
|
},
|
||||||
|
suggestion: {
|
||||||
|
char: '@',
|
||||||
|
pluginKey: MentionPluginKey,
|
||||||
|
command: ({ editor, range, props }) => {
|
||||||
|
// increase range.to by one when the next node is of type "text"
|
||||||
|
// and starts with a space character
|
||||||
|
const nodeAfter = editor.view.state.selection.$to.nodeAfter
|
||||||
|
const overrideSpace = nodeAfter?.text?.startsWith(' ')
|
||||||
|
|
||||||
|
if (overrideSpace) {
|
||||||
|
range.to += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.insertContentAt(range, [
|
||||||
|
{
|
||||||
|
type: this.name,
|
||||||
|
attrs: props,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ' ',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
.run()
|
||||||
|
|
||||||
|
window.getSelection()?.collapseToEnd()
|
||||||
|
},
|
||||||
|
allow: ({ state, range }) => {
|
||||||
|
const $from = state.doc.resolve(range.from)
|
||||||
|
const type = state.schema.nodes[this.name]
|
||||||
|
const allow = !!$from.parent.type.contentMatch.matchType(type)
|
||||||
|
|
||||||
|
return allow
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
group: 'inline',
|
||||||
|
|
||||||
|
inline: true,
|
||||||
|
|
||||||
|
selectable: false,
|
||||||
|
|
||||||
|
atom: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
id: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => element.getAttribute('data-id'),
|
||||||
|
renderHTML: (attributes) => {
|
||||||
|
if (!attributes.id) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'data-id': attributes.id,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
label: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => element.getAttribute('data-label'),
|
||||||
|
renderHTML: (attributes) => {
|
||||||
|
if (!attributes.label) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'data-label': attributes.label,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: `span[data-type="${this.name}"]`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ node, HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
'div',
|
||||||
|
mergeAttributes(
|
||||||
|
{ 'data-type': this.name },
|
||||||
|
{ 'data-element': node.attrs.id.element.slug },
|
||||||
|
{ tabindex: -1 },
|
||||||
|
this.options.HTMLAttributes,
|
||||||
|
HTMLAttributes
|
||||||
|
),
|
||||||
|
this.options.renderLabel({
|
||||||
|
options: this.options,
|
||||||
|
node,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
renderText({ node }) {
|
||||||
|
return this.options.renderLabel({
|
||||||
|
options: this.options,
|
||||||
|
node,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
Backspace: () =>
|
||||||
|
this.editor.commands.command(({ tr, state }) => {
|
||||||
|
let isMention = false
|
||||||
|
const { selection } = state
|
||||||
|
const { empty, anchor } = selection
|
||||||
|
|
||||||
|
if (!empty) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => {
|
||||||
|
if (node.type.name === this.name) {
|
||||||
|
isMention = true
|
||||||
|
tr.insertText('', pos, pos + node.nodeSize)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return isMention
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
CustomSuggestion({
|
||||||
|
editor: this.editor,
|
||||||
|
...this.options.suggestion,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default FilterMention
|
||||||
Loading…
Reference in a new issue