Skip to content

Commit

Permalink
fix: types for Suggestion command, allowing generic overrides (#4136)
Browse files Browse the repository at this point in the history
* Fix typing for Suggestion `command` with new MentionAttrs generic

As of
7cae967,
new generics were added for Suggestion options and props. However,
there is a subtle bug in the current typing: the object selected with
the suggestion `command` need not have the same types as the `items` in
the suggestion options. For instance, in Tiptap's official demo
https://tiptap.dev/api/nodes/mention, the suggestion `items` are all
`string`s, but the selected Mention is of type `{id: string}` (which are
the attributes of the Mention node, as the Mention extension requires):

```ts
  const selectItem = index => {
    const item = props.items[index]

    if (item) {
      props.command({ id: item })
    }
  }
```

i.e., there should be no restriction that when you select something with
the suggestion `command`, it must use the identical structure as the
suggested items. When using the suggestion plugin with the Mention
extension, for instance, the value passed to the SuggestionProps
`props.command()` function must be a `Record<string, any>`, as it's
directly/exclusively used to set the `attrs` of a `Node` via
`insertContentAt` (and you need not use that shape for suggestion
options, as in the Tiptap example above):
https://github.com/ueberdosis/tiptap/blob/44996d60bebd80f3dcc897909f59d83a0eff6337/packages/extension-mention/src/mention.ts#L42
https://github.com/ueberdosis/tiptap/blob/f8695073968c5c6865ad8faf05351020abb2a3cc/packages/core/src/types.ts#L79

This fixes the typing so that suggestions can correctly refer separately
to their own items (of any type), while ensuring the `command`ed item be
of whatever type is necessary (and so in the Mention context, could be
restricted further).

* Add generics to override selected suggestion type

---------

Co-authored-by: Steven DeMartini <sjdemartini@users.noreply.github.com>
  • Loading branch information
sjdemartini and sjdemartini committed May 17, 2024
1 parent 9df8737 commit f55171f
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 18 deletions.
24 changes: 19 additions & 5 deletions packages/extension-mention/src/mention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,21 @@ import { DOMOutputSpec, Node as ProseMirrorNode } from '@tiptap/pm/model'
import { PluginKey } from '@tiptap/pm/state'
import Suggestion, { SuggestionOptions } from '@tiptap/suggestion'

export type MentionOptions = {
// See `addAttributes` below
export interface MentionNodeAttrs {
/**
* The identifier for the selected item that was mentioned, stored as a `data-id`
* attribute.
*/
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;
}

export type MentionOptions<SuggestionItem = any, Attrs extends Record<string, any> = MentionNodeAttrs> = {
/**
* The HTML attributes for a mention node.
* @default {}
Expand All @@ -18,23 +32,23 @@ export type MentionOptions = {
* @returns The label
* @example ({ options, node }) => `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
*/
renderLabel?: (props: { options: MentionOptions; node: ProseMirrorNode }) => string
renderLabel?: (props: { options: MentionOptions<SuggestionItem, Attrs>; node: ProseMirrorNode }) => string

/**
* A function to render the text of a mention.
* @param props The render props
* @returns The text
* @example ({ options, node }) => `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
*/
renderText: (props: { options: MentionOptions; node: ProseMirrorNode }) => string
renderText: (props: { options: MentionOptions<SuggestionItem, Attrs>; node: ProseMirrorNode }) => string

/**
* A function to render the HTML of a mention.
* @param props The render props
* @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}`]
*/
renderHTML: (props: { options: MentionOptions; node: ProseMirrorNode }) => DOMOutputSpec
renderHTML: (props: { options: MentionOptions<SuggestionItem, Attrs>; node: ProseMirrorNode }) => DOMOutputSpec

/**
* Whether to delete the trigger character with backspace.
Expand All @@ -47,7 +61,7 @@ export type MentionOptions = {
* @default {}
* @example { char: '@', pluginKey: MentionPluginKey, command: ({ editor, range, props }) => { ... } }
*/
suggestion: Omit<SuggestionOptions, 'editor'>
suggestion: Omit<SuggestionOptions<SuggestionItem, Attrs>, 'editor'>
}

/**
Expand Down
26 changes: 13 additions & 13 deletions packages/suggestion/src/suggestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Decoration, DecorationSet, EditorView } from '@tiptap/pm/view'

import { findSuggestionMatch as defaultFindSuggestionMatch } from './findSuggestionMatch.js'

export interface SuggestionOptions<I = any> {
export interface SuggestionOptions<I = any, TSelected = any> {
/**
* The plugin key for the suggestion plugin.
* @default 'suggestion'
Expand Down Expand Up @@ -69,7 +69,7 @@ export interface SuggestionOptions<I = any> {
* @returns void
* @example ({ editor, range, props }) => { props.command(props.props) }
*/
command?: (props: { editor: Editor; range: Range; props: I }) => void
command?: (props: { editor: Editor; range: Range; props: TSelected }) => void

/**
* A function that returns the suggestion items in form of an array.
Expand All @@ -86,12 +86,12 @@ export interface SuggestionOptions<I = any> {
* @returns An object with render functions.
*/
render?: () => {
onBeforeStart?: (props: SuggestionProps<I>) => void
onStart?: (props: SuggestionProps<I>) => void
onBeforeUpdate?: (props: SuggestionProps<I>) => void
onUpdate?: (props: SuggestionProps<I>) => void
onExit?: (props: SuggestionProps<I>) => void
onKeyDown?: (props: SuggestionKeyDownProps) => boolean
onBeforeStart?: (props: SuggestionProps<I, TSelected>) => void;
onStart?: (props: SuggestionProps<I, TSelected>) => void;
onBeforeUpdate?: (props: SuggestionProps<I, TSelected>) => void;
onUpdate?: (props: SuggestionProps<I, TSelected>) => void;
onExit?: (props: SuggestionProps<I, TSelected>) => void;
onKeyDown?: (props: SuggestionKeyDownProps) => boolean;
}

/**
Expand All @@ -103,7 +103,7 @@ export interface SuggestionOptions<I = any> {
findSuggestionMatch?: typeof defaultFindSuggestionMatch
}

export interface SuggestionProps<I = any> {
export interface SuggestionProps<I = any, TSelected = any> {
/**
* The editor instance.
*/
Expand Down Expand Up @@ -134,7 +134,7 @@ export interface SuggestionProps<I = any> {
* @param props The props object.
* @returns void
*/
command: (props: I) => void
command: (props: TSelected) => void

/**
* The decoration node HTML element
Expand Down Expand Up @@ -162,7 +162,7 @@ export const SuggestionPluginKey = new PluginKey('suggestion')
* This utility allows you to create suggestions.
* @see https://tiptap.dev/api/utilities/suggestion
*/
export function Suggestion<I = any>({
export function Suggestion<I = any, TSelected = any>({
pluginKey = SuggestionPluginKey,
editor,
char = '@',
Expand All @@ -176,8 +176,8 @@ export function Suggestion<I = any>({
render = () => ({}),
allow = () => true,
findSuggestionMatch = defaultFindSuggestionMatch,
}: SuggestionOptions<I>) {
let props: SuggestionProps<I> | undefined
}: SuggestionOptions<I, TSelected>) {
let props: SuggestionProps<I, TSelected> | undefined
const renderer = render?.()

const plugin: Plugin<any> = new Plugin({
Expand Down

0 comments on commit f55171f

Please sign in to comment.