For sure matches Mention

This commit is contained in:
Eric Bailey
2024-09-09 21:24:32 -05:00
parent bea6627d59
commit 7ae1321608
2 changed files with 80 additions and 52 deletions
@@ -4,10 +4,10 @@
* @see https://github.com/ueberdosis/tiptap/blob/ec6121da1c3f808987d32de7a8c56b52520bfea8/packages/extension-mention/src/mention.ts * @see https://github.com/ueberdosis/tiptap/blob/ec6121da1c3f808987d32de7a8c56b52520bfea8/packages/extension-mention/src/mention.ts
*/ */
import { mergeAttributes, Node } from '@tiptap/core' import {mergeAttributes, Node} from '@tiptap/core'
import { DOMOutputSpec, Node as ProseMirrorNode } from '@tiptap/pm/model' import {DOMOutputSpec, Node as ProseMirrorNode} from '@tiptap/pm/model'
import { PluginKey } from '@tiptap/pm/state' import {PluginKey} from '@tiptap/pm/state'
import Suggestion, { SuggestionOptions } from '@tiptap/suggestion' import Suggestion, {SuggestionOptions} from '@tiptap/suggestion'
import {findSuggestionMatch} from './utils' import {findSuggestionMatch} from './utils'
@@ -17,15 +17,18 @@ export interface MentionNodeAttrs {
* The identifier for the selected item that was mentioned, stored as a `data-id` * The identifier for the selected item that was mentioned, stored as a `data-id`
* attribute. * attribute.
*/ */
id: string | null; id: string | null
/** /**
* The label to be rendered by the editor as the displayed text for this mentioned * The label to be rendered by the editor as the displayed text for this mentioned
* item, if provided. Stored as a `data-label` attribute. See `renderLabel`. * item, if provided. Stored as a `data-label` attribute. See `renderLabel`.
*/ */
label?: string | null; label?: string | null
} }
export type MentionOptions<SuggestionItem = any, Attrs extends Record<string, any> = MentionNodeAttrs> = { export type MentionOptions<
SuggestionItem = any,
Attrs extends Record<string, any> = MentionNodeAttrs,
> = {
/** /**
* The HTML attributes for a mention node. * The HTML attributes for a mention node.
* @default {} * @default {}
@@ -40,7 +43,10 @@ export type MentionOptions<SuggestionItem = any, Attrs extends Record<string, an
* @returns The label * @returns The label
* @example ({ options, node }) => `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` * @example ({ options, node }) => `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
*/ */
renderLabel?: (props: { options: MentionOptions<SuggestionItem, Attrs>; node: ProseMirrorNode }) => string renderLabel?: (props: {
options: MentionOptions<SuggestionItem, Attrs>
node: ProseMirrorNode
}) => string
/** /**
* A function to render the text of a mention. * A function to render the text of a mention.
@@ -48,7 +54,10 @@ export type MentionOptions<SuggestionItem = any, Attrs extends Record<string, an
* @returns The text * @returns The text
* @example ({ options, node }) => `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` * @example ({ options, node }) => `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
*/ */
renderText: (props: { options: MentionOptions<SuggestionItem, Attrs>; node: ProseMirrorNode }) => string renderText: (props: {
options: MentionOptions<SuggestionItem, Attrs>
node: ProseMirrorNode
}) => string
/** /**
* A function to render the HTML of a mention. * A function to render the HTML of a mention.
@@ -56,7 +65,10 @@ export type MentionOptions<SuggestionItem = any, Attrs extends Record<string, an
* @returns The HTML as a ProseMirror DOM Output Spec * @returns The HTML as a ProseMirror DOM Output Spec
* @example ({ options, node }) => ['span', { 'data-type': 'mention' }, `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`] * @example ({ options, node }) => ['span', { 'data-type': 'mention' }, `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`]
*/ */
renderHTML: (props: { options: MentionOptions<SuggestionItem, Attrs>; node: ProseMirrorNode }) => DOMOutputSpec renderHTML: (props: {
options: MentionOptions<SuggestionItem, Attrs>
node: ProseMirrorNode
}) => DOMOutputSpec
/** /**
* Whether to delete the trigger character with backspace. * Whether to delete the trigger character with backspace.
@@ -88,11 +100,11 @@ export const Tags = Node.create<MentionOptions>({
addOptions() { addOptions() {
return { return {
HTMLAttributes: {}, HTMLAttributes: {},
renderText({ options, node }) { renderText({options, node}) {
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
}, },
deleteTriggerWithBackspace: false, deleteTriggerWithBackspace: false,
renderHTML({ options, node }) { renderHTML({options, node}) {
return [ return [
'span', 'span',
mergeAttributes(this.HTMLAttributes, options.HTMLAttributes), mergeAttributes(this.HTMLAttributes, options.HTMLAttributes),
@@ -102,7 +114,7 @@ export const Tags = Node.create<MentionOptions>({
suggestion: { suggestion: {
char: '#', char: '#',
pluginKey: TagsPluginKey, pluginKey: TagsPluginKey,
command: ({ editor, range, props }) => { command: ({editor, range, props}) => {
// increase range.to by one when the next node is of type "text" // increase range.to by one when the next node is of type "text"
// and starts with a space character // and starts with a space character
const nodeAfter = editor.view.state.selection.$to.nodeAfter const nodeAfter = editor.view.state.selection.$to.nodeAfter
@@ -129,19 +141,13 @@ export const Tags = Node.create<MentionOptions>({
window.getSelection()?.collapseToEnd() window.getSelection()?.collapseToEnd()
}, },
allow: ({ state, range }) => { allow: ({state, range}) => {
const $from = state.doc.resolve(range.from) const $from = state.doc.resolve(range.from)
const type = state.schema.nodes[this.name] const type = state.schema.nodes[this.name]
const allow = !!$from.parent.type.contentMatch.matchType(type) const allow = !!$from.parent.type.contentMatch.matchType(type)
console.log({
allow,
$from,
state,
})
return allow return allow
}, },
findSuggestionMatch: findSuggestionMatch findSuggestionMatch: findSuggestionMatch,
}, },
} }
}, },
@@ -194,21 +200,31 @@ export const Tags = Node.create<MentionOptions>({
] ]
}, },
renderHTML({ node, HTMLAttributes }) { renderHTML({node, HTMLAttributes}) {
if (this.options.renderLabel !== undefined) { if (this.options.renderLabel !== undefined) {
console.warn('renderLabel is deprecated use renderText and renderHTML instead') console.warn(
'renderLabel is deprecated use renderText and renderHTML instead',
)
return [ return [
'span', 'span',
mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes), mergeAttributes(
{'data-type': this.name},
this.options.HTMLAttributes,
HTMLAttributes,
),
this.options.renderLabel({ this.options.renderLabel({
options: this.options, options: this.options,
node, node,
}), }),
] ]
} }
const mergedOptions = { ...this.options } const mergedOptions = {...this.options}
mergedOptions.HTMLAttributes = mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes) mergedOptions.HTMLAttributes = mergeAttributes(
{'data-type': this.name},
this.options.HTMLAttributes,
HTMLAttributes,
)
const html = this.options.renderHTML({ const html = this.options.renderHTML({
options: mergedOptions, options: mergedOptions,
node, node,
@@ -217,16 +233,22 @@ export const Tags = Node.create<MentionOptions>({
if (typeof html === 'string') { if (typeof html === 'string') {
return [ return [
'span', 'span',
mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes), mergeAttributes(
{'data-type': this.name},
this.options.HTMLAttributes,
HTMLAttributes,
),
html, html,
] ]
} }
return html return html
}, },
renderText({ node }) { renderText({node}) {
if (this.options.renderLabel !== undefined) { if (this.options.renderLabel !== undefined) {
console.warn('renderLabel is deprecated use renderText and renderHTML instead') console.warn(
'renderLabel is deprecated use renderText and renderHTML instead',
)
return this.options.renderLabel({ return this.options.renderLabel({
options: this.options, options: this.options,
node, node,
@@ -240,30 +262,33 @@ export const Tags = Node.create<MentionOptions>({
addKeyboardShortcuts() { addKeyboardShortcuts() {
return { return {
Backspace: () => this.editor.commands.command(({ tr, state }) => { Backspace: () =>
let isMention = false this.editor.commands.command(({tr, state}) => {
const { selection } = state let isMention = false
const { empty, anchor } = selection 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(
this.options.deleteTriggerWithBackspace ? '' : this.options.suggestion.char || '',
pos,
pos + node.nodeSize,
)
if (!empty) {
return false return false
} }
})
return isMention state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => {
}), if (node.type.name === this.name) {
isMention = true
tr.insertText(
this.options.deleteTriggerWithBackspace
? ''
: this.options.suggestion.char || '',
pos,
pos + node.nodeSize,
)
return false
}
})
return isMention
}),
} }
}, },
@@ -42,9 +42,12 @@ export function findSuggestionMatch({
if (!tag || tag.length === 0 || tag.length > 64) return null if (!tag || tag.length === 0 || tag.length > 64) return null
const leadingSpaceOffset = fullMatch.startsWith(' ') ? 1 : 0
const hashtagOffset = 1
// The absolute position of the match in the document // The absolute position of the match in the document
const from = textFrom + fullMatch.indexOf(tag) const from = textFrom + match.index + leadingSpaceOffset
const to = from + tag.length + 1 const to = from + tag.length + hashtagOffset
// If the $position is located within the matched substring, return that range // If the $position is located within the matched substring, return that range
if (from < $position.pos && to >= $position.pos) { if (from < $position.pos && to >= $position.pos) {
@@ -54,7 +57,7 @@ export function findSuggestionMatch({
to, to,
}, },
query: tag.replace(TRAILING_PUNCTUATION_REGEX, ''), query: tag.replace(TRAILING_PUNCTUATION_REGEX, ''),
text: fullMatch, text: fullMatch.replace(/^\s{1}/, ''),
} }
} }