From 7ae1321608daf9306560a55e99c06110d27f5fcf Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 9 Sep 2024 21:24:32 -0500 Subject: [PATCH] For sure matches Mention --- .../composer/text-input/web/Tags/plugin.tsx | 123 +++++++++++------- .../com/composer/text-input/web/Tags/utils.ts | 9 +- 2 files changed, 80 insertions(+), 52 deletions(-) diff --git a/src/view/com/composer/text-input/web/Tags/plugin.tsx b/src/view/com/composer/text-input/web/Tags/plugin.tsx index d588c5ffc2..81d247b1b9 100644 --- a/src/view/com/composer/text-input/web/Tags/plugin.tsx +++ b/src/view/com/composer/text-input/web/Tags/plugin.tsx @@ -4,10 +4,10 @@ * @see https://github.com/ueberdosis/tiptap/blob/ec6121da1c3f808987d32de7a8c56b52520bfea8/packages/extension-mention/src/mention.ts */ -import { mergeAttributes, Node } from '@tiptap/core' -import { DOMOutputSpec, Node as ProseMirrorNode } from '@tiptap/pm/model' -import { PluginKey } from '@tiptap/pm/state' -import Suggestion, { SuggestionOptions } from '@tiptap/suggestion' +import {mergeAttributes, Node} from '@tiptap/core' +import {DOMOutputSpec, Node as ProseMirrorNode} from '@tiptap/pm/model' +import {PluginKey} from '@tiptap/pm/state' +import Suggestion, {SuggestionOptions} from '@tiptap/suggestion' 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` * attribute. */ - id: string | null; + id: string | null /** * 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`. */ - label?: string | null; + label?: string | null } -export type MentionOptions = MentionNodeAttrs> = { +export type MentionOptions< + SuggestionItem = any, + Attrs extends Record = MentionNodeAttrs, +> = { /** * The HTML attributes for a mention node. * @default {} @@ -40,7 +43,10 @@ export type MentionOptions `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` */ - renderLabel?: (props: { options: MentionOptions; node: ProseMirrorNode }) => string + renderLabel?: (props: { + options: MentionOptions + node: ProseMirrorNode + }) => string /** * A function to render the text of a mention. @@ -48,7 +54,10 @@ export type MentionOptions `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` */ - renderText: (props: { options: MentionOptions; node: ProseMirrorNode }) => string + renderText: (props: { + options: MentionOptions + node: ProseMirrorNode + }) => string /** * A function to render the HTML of a mention. @@ -56,7 +65,10 @@ export type MentionOptions ['span', { 'data-type': 'mention' }, `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`] */ - renderHTML: (props: { options: MentionOptions; node: ProseMirrorNode }) => DOMOutputSpec + renderHTML: (props: { + options: MentionOptions + node: ProseMirrorNode + }) => DOMOutputSpec /** * Whether to delete the trigger character with backspace. @@ -88,11 +100,11 @@ export const Tags = Node.create({ addOptions() { return { HTMLAttributes: {}, - renderText({ options, node }) { + renderText({options, node}) { return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` }, deleteTriggerWithBackspace: false, - renderHTML({ options, node }) { + renderHTML({options, node}) { return [ 'span', mergeAttributes(this.HTMLAttributes, options.HTMLAttributes), @@ -102,7 +114,7 @@ export const Tags = Node.create({ suggestion: { char: '#', pluginKey: TagsPluginKey, - command: ({ editor, range, props }) => { + 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 @@ -129,19 +141,13 @@ export const Tags = Node.create({ window.getSelection()?.collapseToEnd() }, - allow: ({ state, range }) => { + 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) - console.log({ - allow, - $from, - state, - }) - return allow }, - findSuggestionMatch: findSuggestionMatch + findSuggestionMatch: findSuggestionMatch, }, } }, @@ -194,21 +200,31 @@ export const Tags = Node.create({ ] }, - renderHTML({ node, HTMLAttributes }) { + renderHTML({node, HTMLAttributes}) { 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 [ 'span', - mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes), + mergeAttributes( + {'data-type': this.name}, + this.options.HTMLAttributes, + HTMLAttributes, + ), this.options.renderLabel({ options: this.options, 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({ options: mergedOptions, node, @@ -217,16 +233,22 @@ export const Tags = Node.create({ if (typeof html === 'string') { return [ 'span', - mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes), + mergeAttributes( + {'data-type': this.name}, + this.options.HTMLAttributes, + HTMLAttributes, + ), html, ] } return html }, - renderText({ node }) { + renderText({node}) { 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({ options: this.options, node, @@ -240,30 +262,33 @@ export const Tags = Node.create({ 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( - this.options.deleteTriggerWithBackspace ? '' : this.options.suggestion.char || '', - pos, - pos + node.nodeSize, - ) + Backspace: () => + this.editor.commands.command(({tr, state}) => { + let isMention = false + const {selection} = state + const {empty, anchor} = selection + if (!empty) { 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 + }), } }, diff --git a/src/view/com/composer/text-input/web/Tags/utils.ts b/src/view/com/composer/text-input/web/Tags/utils.ts index ae474ca4cc..ef6c1c3699 100644 --- a/src/view/com/composer/text-input/web/Tags/utils.ts +++ b/src/view/com/composer/text-input/web/Tags/utils.ts @@ -42,9 +42,12 @@ export function findSuggestionMatch({ 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 - const from = textFrom + fullMatch.indexOf(tag) - const to = from + tag.length + 1 + const from = textFrom + match.index + leadingSpaceOffset + const to = from + tag.length + hashtagOffset // If the $position is located within the matched substring, return that range if (from < $position.pos && to >= $position.pos) { @@ -54,7 +57,7 @@ export function findSuggestionMatch({ to, }, query: tag.replace(TRAILING_PUNCTUATION_REGEX, ''), - text: fullMatch, + text: fullMatch.replace(/^\s{1}/, ''), } }