@tiptap/suggestion 3.26.1 → 3.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +619 -270
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +193 -2
- package/dist/index.d.ts +193 -2
- package/dist/index.js +624 -270
- package/dist/index.js.map +1 -1
- package/package.json +10 -5
- package/src/__tests__/suggestion.test.ts +837 -0
- package/src/helpers.ts +129 -0
- package/src/plugin/async.ts +89 -0
- package/src/plugin/floating-ui.ts +204 -0
- package/src/plugin/props.ts +94 -0
- package/src/plugin/state.ts +182 -0
- package/src/plugin/view.ts +236 -0
- package/src/suggestion.ts +97 -606
- package/src/types.ts +439 -0
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/suggestion.ts","../src/findSuggestionMatch.ts"],"sourcesContent":["import { exitSuggestion, Suggestion } from './suggestion.js'\n\nexport * from './findSuggestionMatch.js'\nexport * from './suggestion.js'\n\nexport { exitSuggestion }\n\nexport default Suggestion\n","import type { Editor, Range } from '@tiptap/core'\nimport type { EditorState, Transaction } from '@tiptap/pm/state'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\n\nimport type { SuggestionMatch } from './findSuggestionMatch.js'\nimport { findSuggestionMatch as defaultFindSuggestionMatch } from './findSuggestionMatch.js'\n\n/**\n * Returns true if the transaction inserted any whitespace or newline character.\n * Used to determine when a dismissed suggestion should become active again.\n */\nfunction hasInsertedWhitespace(transaction: Transaction): boolean {\n if (!transaction.docChanged) {\n return false\n }\n return transaction.steps.some(step => {\n const slice = (step as any).slice\n if (!slice?.content) {\n return false\n }\n // textBetween with '\\n' as block separator catches both inline spaces and newlines\n const inserted = slice.content.textBetween(0, slice.content.size, '\\n')\n return /\\s/.test(inserted)\n })\n}\n\nexport interface SuggestionOptions<I = any, TSelected = any> {\n /**\n * The plugin key for the suggestion plugin.\n * @default 'suggestion'\n * @example 'mention'\n */\n pluginKey?: PluginKey\n\n /**\n * A function that returns a boolean to indicate if the suggestion should be active.\n * This is useful to prevent suggestions from opening for remote users in collaborative environments.\n * @param props The props object.\n * @param props.editor The editor instance.\n * @param props.range The range of the suggestion.\n * @param props.query The current suggestion query.\n * @param props.text The current suggestion text.\n * @param props.transaction The current transaction.\n * @returns {boolean}\n * @example ({ transaction }) => isChangeOrigin(transaction)\n */\n shouldShow?: (props: {\n editor: Editor\n range: Range\n query: string\n text: string\n transaction: Transaction\n }) => boolean\n\n /**\n * Controls when a dismissed suggestion becomes active again.\n * Return `true` to clear the dismissed context for the current transaction.\n */\n shouldResetDismissed?: (props: {\n editor: Editor\n state: EditorState\n range: Range\n match: Exclude<SuggestionMatch, null>\n transaction: Transaction\n allowSpaces: boolean\n }) => boolean\n\n /**\n * The editor instance.\n * @default null\n */\n editor: Editor\n\n /**\n * The character that triggers the suggestion.\n * @default '@'\n * @example '#'\n */\n char?: string\n\n /**\n * Allow spaces in the suggestion query. Not compatible with `allowToIncludeChar`. Will be disabled if `allowToIncludeChar` is set to `true`.\n * @default false\n * @example true\n */\n allowSpaces?: boolean\n\n /**\n * Allow the character to be included in the suggestion query. Not compatible with `allowSpaces`.\n * @default false\n */\n allowToIncludeChar?: boolean\n\n /**\n * Allow prefixes in the suggestion query.\n * @default [' ']\n * @example [' ', '@']\n */\n allowedPrefixes?: string[] | null\n\n /**\n * Only match suggestions at the start of the line.\n * @default false\n * @example true\n */\n startOfLine?: boolean\n\n /**\n * The tag name of the decoration node.\n * @default 'span'\n * @example 'div'\n */\n decorationTag?: string\n\n /**\n * The class name of the decoration node.\n * @default 'suggestion'\n * @example 'mention'\n */\n decorationClass?: string\n\n /**\n * Creates a decoration with the provided content.\n * @param decorationContent - The content to display in the decoration\n * @default \"\" - Creates an empty decoration if no content provided\n */\n decorationContent?: string\n\n /**\n * The class name of the decoration node when it is empty.\n * @default 'is-empty'\n * @example 'is-empty'\n */\n decorationEmptyClass?: string\n\n /**\n * A function that is called when a suggestion is selected.\n * @param props The props object.\n * @param props.editor The editor instance.\n * @param props.range The range of the suggestion.\n * @param props.props The props of the selected suggestion.\n * @returns void\n * @example ({ editor, range, props }) => { props.command(props.props) }\n */\n command?: (props: { editor: Editor; range: Range; props: TSelected }) => void\n\n /**\n * A function that returns the suggestion items in form of an array.\n * @param props The props object.\n * @param props.editor The editor instance.\n * @param props.query The current suggestion query.\n * @returns An array of suggestion items.\n * @example ({ editor, query }) => [{ id: 1, label: 'John Doe' }]\n */\n items?: (props: { query: string; editor: Editor }) => I[] | Promise<I[]>\n\n /**\n * The render function for the suggestion.\n * @returns An object with render functions.\n */\n render?: () => {\n onBeforeStart?: (props: SuggestionProps<I, TSelected>) => void\n onStart?: (props: SuggestionProps<I, TSelected>) => void\n onBeforeUpdate?: (props: SuggestionProps<I, TSelected>) => void\n onUpdate?: (props: SuggestionProps<I, TSelected>) => void\n onExit?: (props: SuggestionProps<I, TSelected>) => void\n onKeyDown?: (props: SuggestionKeyDownProps) => boolean\n }\n\n /**\n * A function that returns a boolean to indicate if the suggestion should be active.\n * @param props The props object.\n * @returns {boolean}\n */\n allow?: (props: {\n editor: Editor\n state: EditorState\n range: Range\n isActive?: boolean\n }) => boolean\n findSuggestionMatch?: typeof defaultFindSuggestionMatch\n}\n\nexport interface SuggestionProps<I = any, TSelected = any> {\n /**\n * The editor instance.\n */\n editor: Editor\n\n /**\n * The range of the suggestion.\n */\n range: Range\n\n /**\n * The current suggestion query.\n */\n query: string\n\n /**\n * The current suggestion text.\n */\n text: string\n\n /**\n * The suggestion items array.\n */\n items: I[]\n\n /**\n * A function that is called when a suggestion is selected.\n * @param props The props object.\n * @returns void\n */\n command: (props: TSelected) => void\n\n /**\n * The decoration node HTML element\n * @default null\n */\n decorationNode: Element | null\n\n /**\n * The function that returns the client rect\n * @default null\n * @example () => new DOMRect(0, 0, 0, 0)\n */\n clientRect?: (() => DOMRect | null) | null\n}\n\nexport interface SuggestionKeyDownProps {\n view: EditorView\n event: KeyboardEvent\n range: Range\n}\n\nexport const SuggestionPluginKey = new PluginKey('suggestion')\n\n/**\n * This utility allows you to create suggestions.\n * @see https://tiptap.dev/api/utilities/suggestion\n */\nexport function Suggestion<I = any, TSelected = any>({\n pluginKey = SuggestionPluginKey,\n editor,\n char = '@',\n allowSpaces = false,\n allowToIncludeChar = false,\n allowedPrefixes = [' '],\n startOfLine = false,\n decorationTag = 'span',\n decorationClass = 'suggestion',\n decorationContent = '',\n decorationEmptyClass = 'is-empty',\n command = () => null,\n items = () => [],\n render = () => ({}),\n allow = () => true,\n findSuggestionMatch = defaultFindSuggestionMatch,\n shouldShow,\n shouldResetDismissed,\n}: SuggestionOptions<I, TSelected>) {\n let props: SuggestionProps<I, TSelected> | undefined\n const renderer = render?.()\n const effectiveAllowSpaces = allowSpaces && !allowToIncludeChar\n\n // Gets the DOM rectangle corresponding to the current editor cursor anchor position\n // Calculates screen coordinates based on Tiptap's cursor position and converts to a DOMRect object\n const getAnchorClientRect = () => {\n const pos = editor.state.selection.$anchor.pos\n const coords = editor.view.coordsAtPos(pos)\n const { top, right, bottom, left } = coords\n\n try {\n return new DOMRect(left, top, right - left, bottom - top)\n } catch {\n return null\n }\n }\n\n // Helper to create a clientRect callback for a given decoration node.\n // Returns null when no decoration node is present. Uses the pluginKey's\n // state to resolve the current decoration node on demand, avoiding a\n // duplicated implementation in multiple places.\n const clientRectFor = (view: EditorView, decorationNode: Element | null) => {\n if (!decorationNode) {\n return getAnchorClientRect\n }\n\n return () => {\n const state = pluginKey.getState(editor.state)\n const decorationId = state?.decorationId\n const currentDecorationNode = view.dom.querySelector(`[data-decoration-id=\"${decorationId}\"]`)\n\n return currentDecorationNode?.getBoundingClientRect() || null\n }\n }\n\n const shouldKeepDismissed = ({\n match,\n dismissedRange,\n state,\n transaction,\n }: {\n match: Exclude<SuggestionMatch, null>\n dismissedRange: Range\n state: EditorState\n transaction: Transaction\n }) => {\n if (\n shouldResetDismissed?.({\n editor,\n state,\n range: dismissedRange,\n match,\n transaction,\n allowSpaces: effectiveAllowSpaces,\n })\n ) {\n return false\n }\n\n if (effectiveAllowSpaces) {\n return match.range.from === dismissedRange.from\n }\n\n return match.range.from === dismissedRange.from && !hasInsertedWhitespace(transaction)\n }\n\n // small helper used internally by the view to dispatch an exit\n function dispatchExit(view: EditorView, pluginKeyRef: PluginKey) {\n try {\n const state = pluginKey.getState(view.state)\n const decorationNode = state?.decorationId\n ? view.dom.querySelector(`[data-decoration-id=\"${state.decorationId}\"]`)\n : null\n\n const exitProps: SuggestionProps = {\n // @ts-ignore editor is available in closure\n editor,\n range: state?.range || { from: 0, to: 0 },\n query: state?.query || null,\n text: state?.text || null,\n items: [],\n command: commandProps => {\n return command({\n editor,\n range: state?.range || { from: 0, to: 0 },\n props: commandProps as any,\n })\n },\n decorationNode,\n clientRect: clientRectFor(view, decorationNode),\n }\n\n renderer?.onExit?.(exitProps)\n } catch {\n // ignore errors from consumer renderers\n }\n\n const tr = view.state.tr.setMeta(pluginKeyRef, { exit: true })\n // Dispatch a metadata-only transaction to signal the plugin to exit\n view.dispatch(tr)\n }\n\n const plugin: Plugin<any> = new Plugin({\n key: pluginKey,\n\n view() {\n return {\n update: async (view, prevState) => {\n const prev = this.key?.getState(prevState)\n const next = this.key?.getState(view.state)\n\n // See how the state changed\n const moved = prev.active && next.active && prev.range.from !== next.range.from\n const started = !prev.active && next.active\n const stopped = prev.active && !next.active\n const changed = !started && !stopped && prev.query !== next.query\n\n const handleStart = started || (moved && changed)\n const handleChange = changed || moved\n const handleExit = stopped || (moved && changed)\n\n // Cancel when suggestion isn't active\n if (!handleStart && !handleChange && !handleExit) {\n return\n }\n\n const state = handleExit && !handleStart ? prev : next\n const decorationNode = view.dom.querySelector(\n `[data-decoration-id=\"${state.decorationId}\"]`,\n )\n\n props = {\n editor,\n range: state.range,\n query: state.query,\n text: state.text,\n items: [],\n command: commandProps => {\n return command({\n editor,\n range: state.range,\n props: commandProps,\n })\n },\n decorationNode,\n clientRect: clientRectFor(view, decorationNode),\n }\n\n if (handleStart) {\n renderer?.onBeforeStart?.(props)\n }\n\n if (handleChange) {\n renderer?.onBeforeUpdate?.(props)\n }\n\n if (handleChange || handleStart) {\n props.items = await items({\n editor,\n query: state.query,\n })\n }\n\n if (handleExit) {\n renderer?.onExit?.(props)\n }\n\n if (handleChange) {\n renderer?.onUpdate?.(props)\n }\n\n if (handleStart) {\n renderer?.onStart?.(props)\n }\n },\n\n destroy: () => {\n if (!props) {\n return\n }\n\n renderer?.onExit?.(props)\n },\n }\n },\n\n state: {\n // Initialize the plugin's internal state.\n init() {\n const state: {\n active: boolean\n range: Range\n query: null | string\n text: null | string\n composing: boolean\n decorationId?: string | null\n dismissedRange: Range | null\n } = {\n active: false,\n range: {\n from: 0,\n to: 0,\n },\n query: null,\n text: null,\n composing: false,\n dismissedRange: null,\n }\n\n return state\n },\n\n // Apply changes to the plugin state from a view transaction.\n apply(transaction, prev, _oldState, state) {\n const { isEditable } = editor\n const { composing } = editor.view\n const { selection } = transaction\n const { empty, from } = selection\n const next = { ...prev }\n\n // If a transaction carries the exit meta for this plugin, immediately\n // deactivate the suggestion. This allows metadata-only transactions\n // (dispatched by escape or programmatic exit) to deterministically\n // clear decorations without changing the document.\n const meta = transaction.getMeta(pluginKey)\n if (meta && meta.exit) {\n next.active = false\n next.decorationId = null\n next.range = { from: 0, to: 0 }\n next.query = null\n next.text = null\n next.dismissedRange = prev.active ? { ...prev.range } : prev.dismissedRange\n\n return next\n }\n\n next.composing = composing\n\n if (transaction.docChanged && next.dismissedRange !== null) {\n next.dismissedRange = {\n from: transaction.mapping.map(next.dismissedRange.from),\n to: transaction.mapping.map(next.dismissedRange.to),\n }\n }\n\n // We can only be suggesting if the view is editable, and:\n // * there is no selection, or\n // * a composition is active (see: https://github.com/ueberdosis/tiptap/issues/1449)\n if (isEditable && (empty || editor.view.composing)) {\n // Reset active state if we just left the previous suggestion range\n if ((from < prev.range.from || from > prev.range.to) && !composing && !prev.composing) {\n next.active = false\n }\n\n // Try to match against where our cursor currently is\n const match = findSuggestionMatch({\n char,\n allowSpaces,\n allowToIncludeChar,\n allowedPrefixes,\n startOfLine,\n $position: selection.$from,\n })\n const decorationId = `id_${Math.floor(Math.random() * 0xffffffff)}`\n\n // If we found a match, update the current state to show it\n if (\n match &&\n allow({\n editor,\n state,\n range: match.range,\n isActive: prev.active,\n }) &&\n (!shouldShow ||\n shouldShow({\n editor,\n range: match.range,\n query: match.query,\n text: match.text,\n transaction,\n }))\n ) {\n if (\n next.dismissedRange !== null &&\n !shouldKeepDismissed({\n match,\n dismissedRange: next.dismissedRange,\n state,\n transaction,\n })\n ) {\n next.dismissedRange = null\n }\n\n if (next.dismissedRange === null) {\n next.active = true\n next.decorationId = prev.decorationId ? prev.decorationId : decorationId\n next.range = match.range\n next.query = match.query\n next.text = match.text\n } else {\n next.active = false\n }\n } else {\n if (!match) {\n next.dismissedRange = null\n }\n next.active = false\n }\n } else {\n next.active = false\n }\n\n // Make sure to empty the range if suggestion is inactive\n if (!next.active) {\n next.decorationId = null\n next.range = { from: 0, to: 0 }\n next.query = null\n next.text = null\n }\n\n return next\n },\n },\n\n props: {\n // Call the keydown hook if suggestion is active.\n handleKeyDown(view, event) {\n const { active, range } = plugin.getState(view.state)\n\n if (!active) {\n return false\n }\n\n // If Escape is pressed, call onExit and dispatch a metadata-only\n // transaction to unset the suggestion state. This provides a safe\n // and deterministic way to exit the suggestion without altering the\n // document (avoids transaction mapping/mismatch issues).\n if (event.key === 'Escape' || event.key === 'Esc') {\n const state = plugin.getState(view.state)\n\n // Allow the consumer to react to Escape, but always clear the\n // suggestion state afterward so the decoration is removed too.\n renderer?.onKeyDown?.({ view, event, range: state.range })\n\n // dispatch metadata-only transaction to unset the plugin state\n dispatchExit(view, pluginKey)\n\n return true\n }\n\n const handled = renderer?.onKeyDown?.({ view, event, range }) || false\n return handled\n },\n\n // Setup decorator on the currently active suggestion.\n decorations(state) {\n const { active, range, decorationId, query } = plugin.getState(state)\n\n if (!active) {\n return null\n }\n\n const isEmpty = !query?.length\n const classNames = [decorationClass]\n\n if (isEmpty) {\n classNames.push(decorationEmptyClass)\n }\n\n return DecorationSet.create(state.doc, [\n Decoration.inline(range.from, range.to, {\n nodeName: decorationTag,\n class: classNames.join(' '),\n 'data-decoration-id': decorationId,\n 'data-decoration-content': decorationContent,\n }),\n ])\n },\n },\n })\n\n return plugin\n}\n\n/**\n * Programmatically exit a suggestion plugin by dispatching a metadata-only\n * transaction. This is the safe, recommended API to remove suggestion\n * decorations without touching the document or causing mapping errors.\n */\nexport function exitSuggestion(view: EditorView, pluginKeyRef: PluginKey = SuggestionPluginKey) {\n const tr = view.state.tr.setMeta(pluginKeyRef, { exit: true })\n view.dispatch(tr)\n}\n","import type { Range } from '@tiptap/core'\nimport { escapeForRegEx } from '@tiptap/core'\nimport type { ResolvedPos } from '@tiptap/pm/model'\n\nexport interface Trigger {\n char: string\n allowSpaces: boolean\n allowToIncludeChar: boolean\n allowedPrefixes: string[] | null\n startOfLine: boolean\n $position: ResolvedPos\n}\n\nexport type SuggestionMatch = {\n range: Range\n query: string\n text: string\n} | null\n\nexport function findSuggestionMatch(config: Trigger): SuggestionMatch {\n const {\n char,\n allowSpaces: allowSpacesOption,\n allowToIncludeChar,\n allowedPrefixes,\n startOfLine,\n $position,\n } = config\n\n const allowSpaces = allowSpacesOption && !allowToIncludeChar\n\n const escapedChar = escapeForRegEx(char)\n const suffix = new RegExp(`\\\\s${escapedChar}$`)\n const prefix = startOfLine ? '^' : ''\n const finalEscapedChar = allowToIncludeChar ? '' : escapedChar\n const regexp = allowSpaces\n ? new RegExp(`${prefix}${escapedChar}.*?(?=\\\\s${finalEscapedChar}|$)`, 'gm')\n : new RegExp(`${prefix}(?:^)?${escapedChar}[^\\\\s${finalEscapedChar}]*`, 'gm')\n\n const text = $position.nodeBefore?.isText && $position.nodeBefore.text\n\n if (!text) {\n return null\n }\n\n const textFrom = $position.pos - text.length\n const match = Array.from(text.matchAll(regexp)).pop()\n\n if (!match || match.input === undefined || match.index === undefined) {\n return null\n }\n\n // JavaScript doesn't have lookbehinds. This hacks a check that first character\n // is a space or the start of the line\n const matchPrefix = match.input.slice(Math.max(0, match.index - 1), match.index)\n const matchPrefixIsAllowed = new RegExp(`^[${allowedPrefixes?.join('')}\\0]?$`).test(matchPrefix)\n\n if (allowedPrefixes !== null && !matchPrefixIsAllowed) {\n return null\n }\n\n // The absolute position of the match in the document\n const from = textFrom + match.index\n let to = from + match[0].length\n\n // Edge case handling; if spaces are allowed and we're directly in between\n // two triggers\n if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) {\n match[0] += ' '\n to += 1\n }\n\n // If the $position is located within the matched substring, return that range\n if (from < $position.pos && to >= $position.pos) {\n return {\n range: {\n from,\n to,\n },\n query: match[0].slice(char.length),\n text: match[0],\n }\n }\n\n return null\n}\n"],"mappings":"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","names":["findSuggestionMatch"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/suggestion.ts","../src/findSuggestionMatch.ts","../src/helpers.ts","../src/plugin/props.ts","../src/plugin/state.ts","../src/plugin/async.ts","../src/plugin/floating-ui.ts","../src/plugin/view.ts"],"sourcesContent":["import { exitSuggestion, Suggestion } from './suggestion.js'\n\nexport * from './findSuggestionMatch.js'\nexport * from './suggestion.js'\n\nexport { exitSuggestion }\n\nexport default Suggestion\n","import type { Range } from '@tiptap/core'\nimport type { EditorState, Transaction } from '@tiptap/pm/state'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport type { SuggestionMatch } from './findSuggestionMatch.js'\nimport { findSuggestionMatch as defaultFindSuggestionMatch } from './findSuggestionMatch.js'\nimport {\n clientRectFor as clientRectForHelper,\n dispatchExit as dispatchExitHelper,\n shouldKeepDismissed as shouldKeepDismissedHelper,\n} from './helpers.js'\nimport { createSuggestionProps } from './plugin/props.js'\nimport { createSuggestionState } from './plugin/state.js'\nimport { createSuggestionView } from './plugin/view.js'\nimport type { SuggestionOptions } from './types.js'\n\nexport type {\n SuggestionFloatingUiConfig,\n SuggestionFloatingUiOptions,\n SuggestionKeyDownProps,\n SuggestionMount,\n SuggestionMountOptions,\n SuggestionOptions,\n SuggestionPlacement,\n SuggestionPositionData,\n SuggestionProps,\n} from './types.js'\n\nexport const SuggestionPluginKey = new PluginKey('suggestion')\n\ntype ShouldKeepDismissedProps = {\n match: Exclude<SuggestionMatch, null>\n dismissedRange: Range\n state: EditorState\n transaction: Transaction\n}\n\n/**\n * This utility allows you to create suggestions.\n * @see https://tiptap.dev/api/utilities/suggestion\n */\nexport function Suggestion<I = any, TSelected = any>({\n pluginKey = SuggestionPluginKey,\n editor,\n char = '@',\n allowSpaces = false,\n allowToIncludeChar = false,\n allowedPrefixes = [' '],\n startOfLine = false,\n decorationTag = 'span',\n decorationClass = 'suggestion',\n decorationContent = '',\n decorationEmptyClass = 'is-empty',\n command = () => null,\n items = () => [],\n minQueryLength = 0,\n debounce = 0,\n initialItems,\n placement = 'bottom-start',\n offset: offsetOption = {},\n container,\n flip = true,\n floatingUi,\n dismissOnOutsideClick = true,\n render = () => ({}),\n allow = () => true,\n findSuggestionMatch = defaultFindSuggestionMatch,\n shouldShow,\n shouldResetDismissed,\n}: SuggestionOptions<I, TSelected>) {\n const renderer = render?.()\n const effectiveAllowSpaces = allowSpaces && !allowToIncludeChar\n\n const clientRectFor = (view: EditorView, decorationNode: Element | null) =>\n clientRectForHelper(editor, view, decorationNode, pluginKey)\n\n // helper to check if the dismissed suggestion should stay dismissed, with access to editor and options\n function shouldKeepDismissed(props: ShouldKeepDismissedProps) {\n return shouldKeepDismissedHelper({\n ...props,\n editor,\n shouldResetDismissed,\n effectiveAllowSpaces,\n })\n }\n\n const dispatchExit = (view: EditorView) =>\n dispatchExitHelper({\n view,\n pluginKeyRef: pluginKey,\n })\n\n return new Plugin({\n key: pluginKey,\n\n view: () =>\n createSuggestionView({\n editor,\n pluginKey,\n items,\n renderer,\n minQueryLength,\n debounce,\n initialItems,\n placement,\n offset: offsetOption,\n container,\n flip,\n floatingUi,\n dismissOnOutsideClick,\n command,\n clientRectFor,\n dispatchExit,\n }),\n\n state: createSuggestionState({\n editor,\n char,\n effectiveAllowSpaces,\n allowToIncludeChar,\n allowedPrefixes,\n startOfLine,\n findSuggestionMatch,\n allow,\n shouldShow,\n shouldKeepDismissed,\n pluginKey,\n }),\n\n props: createSuggestionProps({\n pluginKey,\n decorationTag,\n decorationClass,\n decorationContent,\n decorationEmptyClass,\n renderer,\n dispatchExit,\n }),\n })\n}\n\n/**\n * Programmatically exit a suggestion plugin by dispatching a metadata-only\n * transaction. This is the safe, recommended API to remove suggestion\n * decorations without touching the document or causing mapping errors.\n */\nexport function exitSuggestion(view: EditorView, pluginKeyRef: PluginKey = SuggestionPluginKey) {\n const tr = view.state.tr.setMeta(pluginKeyRef, { exit: true })\n view.dispatch(tr)\n}\n","import type { Range } from '@tiptap/core'\nimport { escapeForRegEx } from '@tiptap/core'\nimport type { ResolvedPos } from '@tiptap/pm/model'\n\nexport interface Trigger {\n char: string\n allowSpaces: boolean\n allowToIncludeChar: boolean\n allowedPrefixes: string[] | null\n startOfLine: boolean\n $position: ResolvedPos\n}\n\nexport type SuggestionMatch = {\n range: Range\n query: string\n text: string\n} | null\n\nexport function findSuggestionMatch(config: Trigger): SuggestionMatch {\n const {\n char,\n allowSpaces: allowSpacesOption,\n allowToIncludeChar,\n allowedPrefixes,\n startOfLine,\n $position,\n } = config\n\n const allowSpaces = allowSpacesOption && !allowToIncludeChar\n\n const escapedChar = escapeForRegEx(char)\n const suffix = new RegExp(`\\\\s${escapedChar}$`)\n const prefix = startOfLine ? '^' : ''\n const finalEscapedChar = allowToIncludeChar ? '' : escapedChar\n const regexp = allowSpaces\n ? new RegExp(`${prefix}${escapedChar}.*?(?=\\\\s${finalEscapedChar}|$)`, 'gm')\n : new RegExp(`${prefix}(?:^)?${escapedChar}[^\\\\s${finalEscapedChar}]*`, 'gm')\n\n const text = $position.nodeBefore?.isText && $position.nodeBefore.text\n\n if (!text) {\n return null\n }\n\n const textFrom = $position.pos - text.length\n const match = Array.from(text.matchAll(regexp)).pop()\n\n if (!match || match.input === undefined || match.index === undefined) {\n return null\n }\n\n // JavaScript doesn't have lookbehinds. This hacks a check that first character\n // is a space or the start of the line\n const matchPrefix = match.input.slice(Math.max(0, match.index - 1), match.index)\n const matchPrefixIsAllowed = new RegExp(`^[${allowedPrefixes?.join('')}\\0]?$`).test(matchPrefix)\n\n if (allowedPrefixes !== null && !matchPrefixIsAllowed) {\n return null\n }\n\n // The absolute position of the match in the document\n const from = textFrom + match.index\n let to = from + match[0].length\n\n // Edge case handling; if spaces are allowed and we're directly in between\n // two triggers\n if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) {\n match[0] += ' '\n to += 1\n }\n\n // If the $position is located within the matched substring, return that range\n if (from < $position.pos && to >= $position.pos) {\n return {\n range: {\n from,\n to,\n },\n query: match[0].slice(char.length),\n text: match[0],\n }\n }\n\n return null\n}\n","import type { Editor, Range } from '@tiptap/core'\nimport type { EditorState, PluginKey, Transaction } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport type { SuggestionMatch } from './findSuggestionMatch.js'\nimport type { SuggestionOptions, SuggestionPluginState } from './types.js'\n\n/**\n * Returns true if the transaction inserted any whitespace or newline character.\n * Used to determine when a dismissed suggestion should become active again.\n */\nexport function hasInsertedWhitespace(transaction: Transaction): boolean {\n if (!transaction.docChanged) {\n return false\n }\n return transaction.steps.some(step => {\n const slice = (step as any).slice\n if (!slice?.content) {\n return false\n }\n // textBetween with '\\n' as block separator catches both inline spaces and newlines\n const inserted = slice.content.textBetween(0, slice.content.size, '\\n')\n return /\\s/.test(inserted)\n })\n}\n\n/**\n * Gets the DOM rectangle corresponding to the current editor cursor anchor position.\n * Calculates screen coordinates based on Tiptap's cursor position and converts to a DOMRect object.\n */\nexport function getAnchorClientRect(editor: Editor): () => DOMRect | null {\n return () => {\n const pos = editor.state.selection.$anchor.pos\n const coords = editor.view.coordsAtPos(pos)\n const { top, right, bottom, left } = coords\n\n try {\n return new DOMRect(left, top, right - left, bottom - top)\n } catch {\n return null\n }\n }\n}\n\n/**\n * Creates a clientRect callback for a given decoration node.\n * Returns the anchor rect when no decoration node is present.\n * Uses the pluginKey's state to resolve the current decoration node on demand.\n */\nexport function clientRectFor(\n editor: Editor,\n view: EditorView,\n decorationNode: Element | null,\n pluginKey: PluginKey,\n): () => DOMRect | null {\n if (!decorationNode) {\n return getAnchorClientRect(editor)\n }\n\n return () => {\n const state: SuggestionPluginState = pluginKey.getState(editor.state) as any\n const decorationId = state?.decorationId\n const currentDecorationNode = view.dom.querySelector(`[data-decoration-id=\"${decorationId}\"]`)\n\n return currentDecorationNode?.getBoundingClientRect() || null\n }\n}\n\n/**\n * Determines whether a dismissed suggestion should stay dismissed.\n * Returns `true` (keep dismissed) or `false` (allow reactivation).\n */\nexport function shouldKeepDismissed({\n match,\n dismissedRange,\n state,\n transaction,\n editor,\n shouldResetDismissed,\n effectiveAllowSpaces,\n}: {\n match: Exclude<SuggestionMatch, null>\n dismissedRange: Range\n state: EditorState\n transaction: Transaction\n editor: Editor\n shouldResetDismissed?: SuggestionOptions['shouldResetDismissed']\n effectiveAllowSpaces: boolean\n}): boolean {\n if (\n shouldResetDismissed?.({\n editor,\n state,\n range: dismissedRange,\n match,\n transaction,\n allowSpaces: effectiveAllowSpaces,\n })\n ) {\n return false\n }\n\n if (effectiveAllowSpaces) {\n return match.range.from === dismissedRange.from\n }\n\n return match.range.from === dismissedRange.from && !hasInsertedWhitespace(transaction)\n}\n\n/**\n * Dispatch an exit of the suggestion plugin by dispatching a metadata-only\n * transaction to clear the plugin state. The renderer's onExit hook is NOT\n * called here — it fires via the plugin view's stopped transition, which\n * builds SuggestionProps consistently with the normal lifecycle.\n *\n * This prevents a double onExit call (one from dispatchExit, one from the\n * view's update) and keeps exitSuggestion consistent with Escape-triggered\n * exits.\n */\nexport function dispatchExit({\n view,\n pluginKeyRef,\n}: {\n view: EditorView\n pluginKeyRef: PluginKey\n}): void {\n const tr = view.state.tr.setMeta(pluginKeyRef, { exit: true })\n view.dispatch(tr)\n}\n","import type { EditorState, PluginKey } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\n\nimport type { SuggestionKeyDownProps, SuggestionPluginState } from '../types.js'\n\n/**\n * Creates the `props` object for the suggestion ProseMirror plugin.\n * Contains `handleKeyDown` for keyboard handling and `decorations`\n * for rendering the suggestion highlight.\n */\nexport interface CreateSuggestionPropsOptions {\n pluginKey: PluginKey\n decorationTag: string\n decorationClass: string\n decorationContent: string\n decorationEmptyClass: string\n renderer: { onKeyDown?: (props: SuggestionKeyDownProps) => boolean } | undefined\n dispatchExit: (view: EditorView) => void\n}\n\n/**\n * Creates the `props` object for the suggestion ProseMirror plugin.\n * Contains `handleKeyDown` for keyboard handling and `decorations`\n * for rendering the suggestion highlight.\n */\nexport function createSuggestionProps({\n pluginKey,\n decorationTag,\n decorationClass,\n decorationContent,\n decorationEmptyClass,\n renderer,\n dispatchExit,\n}: CreateSuggestionPropsOptions) {\n return {\n /**\n * Call the keydown hook if suggestion is active.\n */\n handleKeyDown(view: EditorView, event: KeyboardEvent) {\n const state: SuggestionPluginState = pluginKey.getState(view.state) as any\n\n if (!state.active) {\n return false\n }\n\n // If Escape is pressed, call onKeyDown and dispatch a metadata-only\n // transaction to unset the suggestion state. This provides a safe\n // and deterministic way to exit the suggestion without altering the\n // document (avoids transaction mapping/mismatch issues).\n if (event.key === 'Escape' || event.key === 'Esc') {\n // Allow the consumer to react to Escape, but always clear the\n // suggestion state afterward so the decoration is removed too.\n renderer?.onKeyDown?.({ view, event, range: state.range })\n\n // dispatch metadata-only transaction to unset the plugin state\n dispatchExit(view)\n\n return true\n }\n\n const handled = renderer?.onKeyDown?.({ view, event, range: state.range }) || false\n return handled\n },\n\n /**\n * Setup decorator on the currently active suggestion.\n */\n decorations(state: EditorState) {\n const pluginState: SuggestionPluginState = pluginKey.getState(state) as any\n const { active, range, decorationId, query } = pluginState\n\n if (!active) {\n return null\n }\n\n const isEmpty = !query?.length\n const classNames = [decorationClass]\n\n if (isEmpty) {\n classNames.push(decorationEmptyClass)\n }\n\n return DecorationSet.create(state.doc, [\n Decoration.inline(range.from, range.to, {\n nodeName: decorationTag,\n class: classNames.join(' '),\n 'data-decoration-id': decorationId || undefined,\n 'data-decoration-content': decorationContent,\n }),\n ])\n },\n }\n}\n","import type { Editor, Range } from '@tiptap/core'\nimport type { EditorState, PluginKey, Transaction } from '@tiptap/pm/state'\n\nimport type {\n findSuggestionMatch as defaultFindSuggestionMatch,\n SuggestionMatch,\n} from '../findSuggestionMatch.js'\nimport type { SuggestionOptions, SuggestionPluginState } from '../types.js'\n\nexport interface CreateSuggestionStateOptions {\n editor: Editor\n char: string\n effectiveAllowSpaces: boolean\n allowToIncludeChar: boolean\n allowedPrefixes: string[] | null\n startOfLine: boolean\n findSuggestionMatch: typeof defaultFindSuggestionMatch\n allow: Exclude<SuggestionOptions['allow'], undefined>\n shouldShow?: SuggestionOptions['shouldShow']\n shouldKeepDismissed: (props: {\n match: Exclude<SuggestionMatch, null>\n dismissedRange: Range\n state: EditorState\n transaction: Transaction\n }) => boolean\n pluginKey: PluginKey\n}\n\n/**\n * Creates the `state` object for the suggestion ProseMirror plugin.\n * Contains `init()` and `apply()` for managing the plugin's internal state\n * across transactions.\n */\nexport function createSuggestionState({\n editor,\n char,\n effectiveAllowSpaces,\n allowToIncludeChar,\n allowedPrefixes,\n startOfLine,\n findSuggestionMatch,\n allow,\n shouldShow,\n shouldKeepDismissed,\n pluginKey,\n}: CreateSuggestionStateOptions) {\n return {\n /**\n * Initialize the plugin's internal state.\n */\n init(): SuggestionPluginState {\n return {\n active: false,\n range: { from: 0, to: 0 },\n query: null,\n text: null,\n composing: false,\n dismissedRange: null,\n }\n },\n\n /**\n * Apply changes to the plugin state from a view transaction.\n */\n apply(\n transaction: Transaction,\n prev: SuggestionPluginState,\n _oldState: EditorState,\n state: EditorState,\n ): SuggestionPluginState {\n const { isEditable } = editor\n const { composing } = editor.view\n const { selection } = transaction\n const { empty, from } = selection\n const next = { ...prev }\n\n // If a transaction carries the exit meta for this plugin, immediately\n // deactivate the suggestion. This allows metadata-only transactions\n // (dispatched by escape or programmatic exit) to deterministically\n // clear decorations without changing the document.\n const meta = transaction.getMeta(pluginKey)\n if (meta && meta.exit) {\n next.active = false\n next.decorationId = null\n next.range = { from: 0, to: 0 }\n next.query = null\n next.text = null\n next.dismissedRange = prev.active ? { ...prev.range } : prev.dismissedRange\n\n return next\n }\n\n next.composing = composing\n\n if (transaction.docChanged && next.dismissedRange !== null) {\n next.dismissedRange = {\n from: transaction.mapping.map(next.dismissedRange.from),\n to: transaction.mapping.map(next.dismissedRange.to),\n }\n }\n\n // We can only be suggesting if the view is editable, and:\n // * there is no selection, or\n // * a composition is active (see: https://github.com/ueberdosis/tiptap/issues/1449)\n if (isEditable && (empty || editor.view.composing)) {\n // Reset active state if we just left the previous suggestion range\n if ((from < prev.range.from || from > prev.range.to) && !composing && !prev.composing) {\n next.active = false\n }\n\n // Try to match against where our cursor currently is\n const match = findSuggestionMatch({\n char,\n allowSpaces: effectiveAllowSpaces,\n allowToIncludeChar,\n allowedPrefixes,\n startOfLine,\n $position: selection.$from,\n })\n const decorationId = `id_${Math.floor(Math.random() * 0xffffffff)}`\n\n // If we found a match, update the current state to show it\n if (\n match &&\n allow({\n editor,\n state,\n range: match.range,\n isActive: prev.active,\n }) &&\n (!shouldShow ||\n shouldShow({\n editor,\n range: match.range,\n query: match.query,\n text: match.text,\n transaction,\n }))\n ) {\n if (\n next.dismissedRange !== null &&\n !shouldKeepDismissed({\n match,\n dismissedRange: next.dismissedRange,\n state,\n transaction,\n })\n ) {\n next.dismissedRange = null\n }\n\n if (next.dismissedRange === null) {\n next.active = true\n next.decorationId = prev.decorationId || decorationId\n next.range = match.range\n next.query = match.query\n next.text = match.text\n } else {\n next.active = false\n }\n } else {\n if (!match) {\n next.dismissedRange = null\n }\n next.active = false\n }\n } else {\n next.active = false\n }\n\n // Make sure to empty the range if suggestion is inactive\n if (!next.active) {\n next.decorationId = null\n next.range = { from: 0, to: 0 }\n next.query = null\n next.text = null\n }\n\n return next\n },\n }\n}\n","import type { Editor } from '@tiptap/core'\n\nimport type { SuggestionOptions } from '../types.js'\n\nexport interface CreateSuggestionAsyncRequestManagerOptions<I = any> {\n editor: Editor\n items: NonNullable<SuggestionOptions<I>['items']>\n}\n\ntype AsyncRequestResult<I> =\n | { status: 'resolved'; items: I[] }\n | { status: 'aborted' }\n | { status: 'error' }\n\nexport function createSuggestionAsyncRequestManager<I = any>({\n editor,\n items,\n}: CreateSuggestionAsyncRequestManagerOptions<I>) {\n let abortController: AbortController | null = null\n let debounceTimer: ReturnType<typeof setTimeout> | null = null\n let debounceResolve: (() => void) | null = null\n\n const clearDebounceTimer = () => {\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer)\n debounceTimer = null\n }\n\n debounceResolve?.()\n debounceResolve = null\n }\n\n const waitForDebounce = (delay: number) => {\n return new Promise<void>(resolve => {\n debounceResolve = resolve\n debounceTimer = setTimeout(() => {\n debounceTimer = null\n const pendingResolve = debounceResolve\n debounceResolve = null\n pendingResolve?.()\n }, delay)\n })\n }\n\n const abort = () => {\n abortController?.abort()\n clearDebounceTimer()\n abortController = null\n }\n\n const fetch = async (query: string, debounce: number): Promise<AsyncRequestResult<I>> => {\n abort()\n abortController = new AbortController()\n const controller = abortController\n\n if (debounce > 0) {\n await waitForDebounce(debounce)\n }\n\n if (abortController !== controller || controller.signal.aborted) {\n return { status: 'aborted' }\n }\n\n try {\n const result = await items({\n editor,\n query,\n signal: controller.signal,\n })\n\n if (abortController !== controller || controller.signal.aborted) {\n return { status: 'aborted' }\n }\n\n return { status: 'resolved', items: result }\n } catch {\n if (abortController !== controller || controller.signal.aborted) {\n return { status: 'aborted' }\n }\n\n return { status: 'error' }\n }\n }\n\n return {\n abort,\n fetch,\n }\n}\n","import type { Middleware, VirtualElement } from '@floating-ui/dom'\nimport {\n autoUpdate,\n computePosition,\n flip as floatingUiFlip,\n offset as floatingUiOffset,\n} from '@floating-ui/dom'\n\nimport type {\n SuggestionFloatingUiConfig,\n SuggestionFloatingUiOptions,\n SuggestionMount,\n SuggestionPlacement,\n} from '../types.js'\n\nexport interface CreateSuggestionFloatingUiConfigOptions {\n placement: SuggestionPlacement\n offset: { mainAxis?: number; crossAxis?: number }\n flip: boolean\n floatingUi?: SuggestionFloatingUiOptions\n}\n\nexport function createSuggestionFloatingUiConfig({\n placement,\n offset,\n flip,\n floatingUi,\n}: CreateSuggestionFloatingUiConfigOptions): SuggestionFloatingUiConfig {\n const middleware: Middleware[] = [\n floatingUiOffset({\n mainAxis: offset.mainAxis ?? 4,\n crossAxis: offset.crossAxis ?? 0,\n }),\n ]\n\n if (flip) {\n middleware.push(floatingUiFlip())\n }\n\n if (floatingUi?.middleware?.length) {\n middleware.push(...floatingUi.middleware)\n }\n\n return {\n placement,\n strategy: floatingUi?.strategy ?? 'absolute',\n middleware,\n }\n}\n\nexport interface CreateMountOptions {\n /** Returns the current cursor/anchor rect the popup should track. */\n getReferenceRect: () => DOMRect | null\n /**\n * An element inside the editor's layout/scroll context. Floating UI walks up\n * from here to discover the scroll ancestors (and the window) to observe, so\n * the scroll container does not need to be configured manually.\n */\n contextElement: Element\n /** Resolved Floating UI config (placement, strategy, middleware). */\n config: SuggestionFloatingUiConfig\n /**\n * CSS selector or element the popup should be mounted into. Defaults to\n * `document.body`. Used to portal the popup inside dialogs/modals so it\n * renders on top of (and clips within) the right context.\n */\n container?: string | HTMLElement\n /**\n * When `true`, a pointerdown outside both the popup and the editor dismisses\n * the suggestion. Wired up and torn down alongside the mounted element.\n */\n dismissOnOutsideClick: boolean\n /** Dismisses the active suggestion (used by outside-click handling). */\n dismiss: () => void\n}\n\n/**\n * Resolves a container option (selector or element) to a mount target,\n * falling back to `document.body` when it can't be resolved.\n */\nfunction resolveContainer(container?: string | HTMLElement): HTMLElement {\n if (container instanceof HTMLElement) {\n return container\n }\n\n if (typeof container === 'string') {\n try {\n // `container` is consumer-provided; an invalid selector throws a\n // DOMException, so fall back to document.body instead of crashing.\n const found = document.querySelector<HTMLElement>(container)\n\n if (found) {\n return found\n }\n } catch {\n return document.body\n }\n }\n\n return document.body\n}\n\n/**\n * Builds the `mount` function handed to the renderer on `SuggestionProps`.\n *\n * Mounts the popup into the container, then wires Floating UI's `autoUpdate`\n * against a virtual reference that re-reads the live cursor rect, so the popup\n * stays anchored across scroll, resize, and layout shifts without the consumer\n * attaching any listeners. The returned `unmount` tears all of that down.\n */\nexport function createMount({\n getReferenceRect,\n contextElement,\n config,\n container,\n dismissOnOutsideClick,\n dismiss,\n}: CreateMountOptions): SuggestionMount {\n return (element, options = {}) => {\n const reference: VirtualElement = {\n getBoundingClientRect: () => getReferenceRect() ?? new DOMRect(),\n contextElement,\n }\n\n let positioned = false\n\n // Mount the popup into the container (default `document.body`) unless the\n // consumer already placed it in the DOM themselves — in which case we leave\n // mounting (and unmounting) to them.\n const mountedByUs = !element.isConnected\n\n if (mountedByUs) {\n resolveContainer(container).appendChild(element)\n }\n\n // Hide the element until the first measurement resolves so it doesn't flash\n // at its initial coordinates. Skipped when the consumer owns applying the\n // position via `onPosition`.\n if (!options.onPosition) {\n element.style.visibility = 'hidden'\n element.style.width = 'max-content'\n }\n\n const update = () => {\n computePosition(reference, element, {\n placement: config.placement,\n strategy: config.strategy,\n middleware: config.middleware,\n }).then(({ x, y, placement, strategy }) => {\n if (options.onPosition) {\n options.onPosition({ x, y, placement: placement as SuggestionPlacement, strategy })\n return\n }\n\n Object.assign(element.style, {\n position: strategy,\n left: `${x}px`,\n top: `${y}px`,\n })\n\n if (!positioned) {\n positioned = true\n element.style.visibility = ''\n }\n })\n }\n\n const cleanupAutoUpdate = autoUpdate(reference, element, update, options.autoUpdate)\n\n // Dismiss when the user interacts outside both the popup and the editor.\n // Capture phase so a parent that stops propagation can't swallow it.\n let onOutsidePointerDown: ((event: PointerEvent) => void) | undefined\n\n if (dismissOnOutsideClick) {\n onOutsidePointerDown = event => {\n const target = event.target\n\n if (\n !(target instanceof Node) ||\n element.contains(target) ||\n contextElement.contains(target)\n ) {\n return\n }\n\n dismiss()\n }\n\n document.addEventListener('pointerdown', onOutsidePointerDown, true)\n }\n\n return () => {\n cleanupAutoUpdate()\n\n if (onOutsidePointerDown) {\n document.removeEventListener('pointerdown', onOutsidePointerDown, true)\n }\n\n if (mountedByUs) {\n element.remove()\n }\n }\n }\n}\n","import type { Editor } from '@tiptap/core'\nimport type { EditorState, PluginKey } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport type {\n PluginState,\n SuggestionFloatingUiOptions,\n SuggestionOptions,\n SuggestionPlacement,\n SuggestionProps,\n} from '../types.js'\nimport { createSuggestionAsyncRequestManager } from './async.js'\nimport { createMount, createSuggestionFloatingUiConfig } from './floating-ui.js'\n\nexport interface CreateSuggestionViewOptions {\n editor: Editor\n pluginKey: PluginKey<PluginState>\n items: NonNullable<SuggestionOptions['items']>\n renderer: ReturnType<NonNullable<SuggestionOptions['render']>> | undefined\n minQueryLength: number\n debounce: number\n initialItems?: any[]\n placement: SuggestionPlacement\n offset: { mainAxis?: number; crossAxis?: number }\n container?: string | HTMLElement\n flip: boolean\n floatingUi?: SuggestionFloatingUiOptions\n dismissOnOutsideClick: boolean\n command: NonNullable<SuggestionOptions['command']>\n clientRectFor: (view: EditorView, decorationNode: Element | null) => () => DOMRect | null\n dispatchExit: (view: EditorView) => void\n}\n\n/**\n * Creates the `view` object for the suggestion ProseMirror plugin.\n *\n * Manages the async lifecycle: tracks state transitions, calls renderer hooks,\n * fetches items with debounce and AbortController support.\n *\n * 1. Tracks plugin state transitions (started, updated, stopped) to determine when to call renderer hooks.\n * 2. Calls `onBeforeStart`, `onBeforeUpdate`, `onStart` before fetching to allow the renderer to prepare for first render\n * 3. Manages async fetching of suggestion items with support for debouncing and aborting in-flight requests\n * 4. Calls `onUpdate` after fetching new items to update the renderer with the latest data\n * 5. At the end calls a final `onExit` or `onUpdate` to allow the renderer to clean up or finalize the state\n */\nexport function createSuggestionView({\n editor,\n pluginKey,\n items,\n renderer,\n minQueryLength,\n debounce,\n initialItems,\n placement,\n offset: offsetOption,\n container,\n flip,\n floatingUi,\n dismissOnOutsideClick,\n command,\n clientRectFor,\n dispatchExit,\n}: CreateSuggestionViewOptions) {\n let props: SuggestionProps | undefined\n const asyncRequest = createSuggestionAsyncRequestManager({\n editor,\n items,\n })\n const floatingUiConfig = createSuggestionFloatingUiConfig({\n placement,\n offset: offsetOption,\n flip,\n floatingUi,\n })\n\n function dispatchStateUpdate(\n state: 'started' | 'updated' | 'stopped',\n dispatchProps: SuggestionProps,\n ) {\n switch (state) {\n case 'started':\n renderer?.onStart?.(dispatchProps)\n break\n case 'updated':\n renderer?.onUpdate?.(dispatchProps)\n break\n case 'stopped':\n renderer?.onExit?.(dispatchProps)\n break\n default:\n break\n }\n }\n\n return {\n update: async (view: EditorView, prevState: EditorState) => {\n const prev = pluginKey.getState(prevState)\n const next = pluginKey.getState(view.state)\n\n if (!prev || !next) {\n return\n }\n\n let currentState: 'started' | 'updated' | 'stopped' | null = null\n const queryChanged = prev.query !== next.query\n const textChanged = prev.text !== next.text\n const rangeChanged = prev.range.from !== next.range.from || prev.range.to !== next.range.to\n const effectiveQueryChanged = queryChanged || textChanged || rangeChanged\n\n if (!prev.active && next.active) {\n currentState = 'started'\n } else if (prev.active && !next.active) {\n currentState = 'stopped'\n } else if (next.active && effectiveQueryChanged) {\n currentState = 'updated'\n } else {\n return\n }\n\n const state = currentState === 'stopped' ? prev : next\n const decorationNode = view.dom.querySelector(`[data-decoration-id=\"${state.decorationId}\"]`)\n const clientRect = clientRectFor(view, decorationNode)\n\n const exceedsMinQueryLength =\n minQueryLength === 0 || (state.query ? state.query.length >= minQueryLength : false)\n const willFetch =\n (currentState === 'started' || currentState === 'updated') && exceedsMinQueryLength\n\n props = {\n editor,\n range: state.range,\n query: state.query || '',\n text: state.text || '',\n items: initialItems ?? [],\n command: commandProps => {\n return command({\n editor,\n range: state.range,\n props: commandProps,\n })\n },\n decorationNode,\n clientRect,\n loading: willFetch,\n placement,\n offset: { mainAxis: offsetOption.mainAxis ?? 4, crossAxis: offsetOption.crossAxis ?? 0 },\n container,\n flip,\n floatingUi: floatingUiConfig,\n mount: createMount({\n getReferenceRect: clientRect,\n contextElement: view.dom,\n config: floatingUiConfig,\n container,\n dismissOnOutsideClick,\n dismiss: () => dispatchExit(editor.view),\n }),\n }\n\n if (currentState === 'started') {\n renderer?.onBeforeStart?.(props)\n }\n\n if (currentState === 'updated') {\n renderer?.onBeforeUpdate?.(props)\n }\n\n // we run the start before we fetch\n // to allow for the component to render immediately\n if (currentState === 'started') {\n dispatchStateUpdate(currentState, props)\n }\n\n if (currentState === 'started' || currentState === 'updated') {\n if (!willFetch) {\n // Abort any in-flight request so stale results don't overwrite\n asyncRequest.abort()\n props = { ...props, items: initialItems ?? [], loading: false }\n } else {\n // update the renderer with loading state before we start the async fetch\n props = { ...props, items: initialItems ?? [], loading: true }\n currentState = 'updated'\n dispatchStateUpdate(currentState, props)\n\n const result = await asyncRequest.fetch(state.query || '', debounce)\n\n if (result.status === 'aborted') {\n return\n }\n\n // Re-check plugin state because the suggestion may have been dismissed\n const currentPluginState = pluginKey.getState(view.state)\n if (!currentPluginState?.active) {\n asyncRequest.abort()\n\n return\n }\n\n props =\n result.status === 'resolved'\n ? {\n ...props,\n items: result.items,\n loading: false,\n }\n : {\n ...props,\n loading: false,\n }\n }\n }\n\n if (currentState === 'stopped') {\n // stop running updates immediately and call onExit to allow the renderer to clean up\n asyncRequest.abort()\n dispatchStateUpdate(currentState, props)\n props = undefined\n return\n }\n\n if (currentState === 'updated') {\n dispatchStateUpdate(currentState, props)\n }\n },\n\n destroy: () => {\n asyncRequest.abort()\n\n if (!props) {\n return\n }\n\n renderer?.onExit?.(props)\n },\n }\n}\n"],"mappings":"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","names":["dispatchExit","findSuggestionMatch","shouldKeepDismissed","floatingUiOffset","floatingUiFlip","clientRectFor","dispatchExit","findSuggestionMatch","clientRectFor","shouldKeepDismissed","dispatchExit"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Middleware, AutoUpdateOptions } from '@floating-ui/dom';
|
|
1
2
|
import { Range, Editor } from '@tiptap/core';
|
|
2
3
|
import { PluginKey, Transaction, EditorState, Plugin } from '@tiptap/pm/state';
|
|
3
4
|
import { EditorView } from '@tiptap/pm/view';
|
|
@@ -18,6 +19,56 @@ type SuggestionMatch = {
|
|
|
18
19
|
} | null;
|
|
19
20
|
declare function findSuggestionMatch(config: Trigger): SuggestionMatch;
|
|
20
21
|
|
|
22
|
+
type SuggestionPlacement = 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end';
|
|
23
|
+
type SuggestionFloatingUiOptions = {
|
|
24
|
+
strategy?: 'absolute' | 'fixed';
|
|
25
|
+
middleware?: Middleware[];
|
|
26
|
+
};
|
|
27
|
+
type SuggestionFloatingUiConfig = {
|
|
28
|
+
placement: SuggestionPlacement;
|
|
29
|
+
strategy: 'absolute' | 'fixed';
|
|
30
|
+
middleware: Middleware[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The computed position handed to a custom `onPosition` callback when using
|
|
34
|
+
* managed positioning via {@link SuggestionProps.mount}.
|
|
35
|
+
*/
|
|
36
|
+
type SuggestionPositionData = {
|
|
37
|
+
x: number;
|
|
38
|
+
y: number;
|
|
39
|
+
placement: SuggestionPlacement;
|
|
40
|
+
strategy: 'absolute' | 'fixed';
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Options for managed mounting + positioning via {@link SuggestionProps.mount}.
|
|
44
|
+
*/
|
|
45
|
+
type SuggestionMountOptions = {
|
|
46
|
+
/**
|
|
47
|
+
* Override how the computed position is applied to the element.
|
|
48
|
+
* When provided, the plugin stops writing `style.left`/`style.top` itself and
|
|
49
|
+
* hands you the computed coordinates so you can apply them however you want
|
|
50
|
+
* (custom transforms, animation, writing to a framework ref, etc.).
|
|
51
|
+
*/
|
|
52
|
+
onPosition?: (data: SuggestionPositionData) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Options forwarded to Floating UI's `autoUpdate`. Use this to opt into
|
|
55
|
+
* `animationFrame` polling for anchors that move inside transformed or
|
|
56
|
+
* animated containers, or to disable specific observers.
|
|
57
|
+
* @see https://floating-ui.com/docs/autoUpdate
|
|
58
|
+
*/
|
|
59
|
+
autoUpdate?: AutoUpdateOptions;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Mounts a floating element and takes over its positioning. The plugin appends
|
|
63
|
+
* the element into the configured `container` (default `document.body`), keeps
|
|
64
|
+
* it anchored to the suggestion's cursor rect, and automatically repositions it
|
|
65
|
+
* on scroll, resize, and layout shifts via Floating UI's `autoUpdate` — no
|
|
66
|
+
* manual listeners required.
|
|
67
|
+
*
|
|
68
|
+
* Returns an `unmount` function that tears down the listeners and removes the
|
|
69
|
+
* element (when the plugin mounted it). Call it from `onExit`.
|
|
70
|
+
*/
|
|
71
|
+
type SuggestionMount = (element: HTMLElement, options?: SuggestionMountOptions) => () => void;
|
|
21
72
|
interface SuggestionOptions<I = any, TSelected = any> {
|
|
22
73
|
/**
|
|
23
74
|
* The plugin key for the suggestion plugin.
|
|
@@ -128,6 +179,67 @@ interface SuggestionOptions<I = any, TSelected = any> {
|
|
|
128
179
|
range: Range;
|
|
129
180
|
props: TSelected;
|
|
130
181
|
}) => void;
|
|
182
|
+
/**
|
|
183
|
+
* Minimum query length before `items()` is called.
|
|
184
|
+
* When the query is shorter, empty items are passed to the renderer.
|
|
185
|
+
* @default 0 (no filter, same as before)
|
|
186
|
+
* @example 2
|
|
187
|
+
*/
|
|
188
|
+
minQueryLength?: number;
|
|
189
|
+
/**
|
|
190
|
+
* Debounce in milliseconds. When set, `items()` will only be called
|
|
191
|
+
* after the user stops typing for this duration.
|
|
192
|
+
* @default 0 (no debounce, same as before)
|
|
193
|
+
* @example 300
|
|
194
|
+
*/
|
|
195
|
+
debounce?: number;
|
|
196
|
+
/**
|
|
197
|
+
* Items shown immediately when the suggestion popup opens,
|
|
198
|
+
* before the async `items()` call resolves.
|
|
199
|
+
* Useful for showing recent or popular items while loading.
|
|
200
|
+
* @default undefined (no pre-populated items)
|
|
201
|
+
*/
|
|
202
|
+
initialItems?: I[];
|
|
203
|
+
/**
|
|
204
|
+
* Placement of the popup relative to the cursor.
|
|
205
|
+
* Consumers can read this from `SuggestionProps` to configure their positioning library.
|
|
206
|
+
* @default 'bottom-start'
|
|
207
|
+
*/
|
|
208
|
+
placement?: SuggestionPlacement;
|
|
209
|
+
/**
|
|
210
|
+
* Offset of the popup in pixels.
|
|
211
|
+
* Consumers can read this from `SuggestionProps` to configure their positioning library.
|
|
212
|
+
* @default { mainAxis: 4, crossAxis: 0 }
|
|
213
|
+
*/
|
|
214
|
+
offset?: {
|
|
215
|
+
mainAxis?: number;
|
|
216
|
+
crossAxis?: number;
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* CSS selector or element that defines the containment context for the popup.
|
|
220
|
+
* Consumers can read this from `SuggestionProps` when rendering inside modals or dialogs.
|
|
221
|
+
* @default undefined (no containment)
|
|
222
|
+
*/
|
|
223
|
+
container?: string | HTMLElement;
|
|
224
|
+
/**
|
|
225
|
+
* Whether the popup should automatically flip when there isn't enough space.
|
|
226
|
+
* Consumers can read this from `SuggestionProps` to configure their positioning library.
|
|
227
|
+
* @default true
|
|
228
|
+
*/
|
|
229
|
+
flip?: boolean;
|
|
230
|
+
/**
|
|
231
|
+
* Additional Floating UI options and middleware passed through to the renderer.
|
|
232
|
+
* The plugin keeps ownership of the anchor and placement, but consumers can
|
|
233
|
+
* append custom middleware here.
|
|
234
|
+
*/
|
|
235
|
+
floatingUi?: SuggestionFloatingUiOptions;
|
|
236
|
+
/**
|
|
237
|
+
* Dismiss the suggestion when the user interacts outside both the popup and
|
|
238
|
+
* the editor. Only applies when using managed mounting via
|
|
239
|
+
* {@link SuggestionProps.mount} (the plugin needs to know the popup element).
|
|
240
|
+
* @default true
|
|
241
|
+
*/
|
|
242
|
+
dismissOnOutsideClick?: boolean;
|
|
131
243
|
/**
|
|
132
244
|
* A function that returns the suggestion items in form of an array.
|
|
133
245
|
* @param props The props object.
|
|
@@ -139,6 +251,7 @@ interface SuggestionOptions<I = any, TSelected = any> {
|
|
|
139
251
|
items?: (props: {
|
|
140
252
|
query: string;
|
|
141
253
|
editor: Editor;
|
|
254
|
+
signal: AbortSignal;
|
|
142
255
|
}) => I[] | Promise<I[]>;
|
|
143
256
|
/**
|
|
144
257
|
* The render function for the suggestion.
|
|
@@ -165,6 +278,9 @@ interface SuggestionOptions<I = any, TSelected = any> {
|
|
|
165
278
|
}) => boolean;
|
|
166
279
|
findSuggestionMatch?: typeof findSuggestionMatch;
|
|
167
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* The props passed to the suggestion's render functions (onStart, onUpdate, onExit).
|
|
283
|
+
*/
|
|
168
284
|
interface SuggestionProps<I = any, TSelected = any> {
|
|
169
285
|
/**
|
|
170
286
|
* The editor instance.
|
|
@@ -203,18 +319,93 @@ interface SuggestionProps<I = any, TSelected = any> {
|
|
|
203
319
|
* @example () => new DOMRect(0, 0, 0, 0)
|
|
204
320
|
*/
|
|
205
321
|
clientRect?: (() => DOMRect | null) | null;
|
|
322
|
+
/**
|
|
323
|
+
* Placement of the popup relative to the cursor.
|
|
324
|
+
* @default 'bottom-start'
|
|
325
|
+
*/
|
|
326
|
+
placement: SuggestionPlacement;
|
|
327
|
+
/**
|
|
328
|
+
* Offset of the popup in pixels.
|
|
329
|
+
* @default { mainAxis: 4, crossAxis: 0 }
|
|
330
|
+
*/
|
|
331
|
+
offset: {
|
|
332
|
+
mainAxis: number;
|
|
333
|
+
crossAxis: number;
|
|
334
|
+
};
|
|
335
|
+
/**
|
|
336
|
+
* CSS selector or element that defines the containment context for the popup.
|
|
337
|
+
* @default undefined
|
|
338
|
+
*/
|
|
339
|
+
container?: string | HTMLElement;
|
|
340
|
+
/**
|
|
341
|
+
* Whether the popup should automatically flip when there isn't enough space.
|
|
342
|
+
* @default true
|
|
343
|
+
*/
|
|
344
|
+
flip: boolean;
|
|
345
|
+
/**
|
|
346
|
+
* Resolved Floating UI config for direct use with `computePosition()`.
|
|
347
|
+
* This is the escape hatch: reach for it only when you mount the element
|
|
348
|
+
* yourself and want to run the positioning loop manually instead of using
|
|
349
|
+
* {@link SuggestionProps.mount}.
|
|
350
|
+
*/
|
|
351
|
+
floatingUi: SuggestionFloatingUiConfig;
|
|
352
|
+
/**
|
|
353
|
+
* Mounts your floating element and takes over positioning — the recommended,
|
|
354
|
+
* default way to render a suggestion popup.
|
|
355
|
+
*
|
|
356
|
+
* Pass the element you rendered (e.g. from `ReactRenderer`/`VueRenderer`). The
|
|
357
|
+
* plugin appends it into the configured `container` (default `document.body`),
|
|
358
|
+
* keeps it anchored to the cursor, and repositions it on scroll, resize, and
|
|
359
|
+
* layout shifts — no manual listeners required. Returns an `unmount` function
|
|
360
|
+
* that tears everything down; call it in `onExit`.
|
|
361
|
+
*
|
|
362
|
+
* Escape hatch: mount the element yourself and skip this, then run your own
|
|
363
|
+
* positioning loop with {@link SuggestionProps.floatingUi} +
|
|
364
|
+
* {@link SuggestionProps.clientRect}.
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* ```ts
|
|
368
|
+
* onStart: props => {
|
|
369
|
+
* component = new ReactRenderer(DropdownList, { props, editor: props.editor })
|
|
370
|
+
* unmount = props.mount(component.element)
|
|
371
|
+
* },
|
|
372
|
+
* onExit: () => unmount?.(),
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
mount: SuggestionMount;
|
|
376
|
+
/**
|
|
377
|
+
* Whether the items are currently being loaded.
|
|
378
|
+
* `true` before the async `items()` call resolves.
|
|
379
|
+
* Useful for showing a loading spinner or skeleton.
|
|
380
|
+
* @default false
|
|
381
|
+
*/
|
|
382
|
+
loading: boolean;
|
|
206
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* The props passed to the suggestion's onKeyDown render function
|
|
386
|
+
*/
|
|
207
387
|
interface SuggestionKeyDownProps {
|
|
208
388
|
view: EditorView;
|
|
209
389
|
event: KeyboardEvent;
|
|
210
390
|
range: Range;
|
|
211
391
|
}
|
|
392
|
+
/** @internal Internal state shape for the suggestion plugin. */
|
|
393
|
+
interface SuggestionPluginState {
|
|
394
|
+
active: boolean;
|
|
395
|
+
range: Range;
|
|
396
|
+
query: null | string;
|
|
397
|
+
text: null | string;
|
|
398
|
+
composing: boolean;
|
|
399
|
+
decorationId?: string | null;
|
|
400
|
+
dismissedRange: Range | null;
|
|
401
|
+
}
|
|
402
|
+
|
|
212
403
|
declare const SuggestionPluginKey: PluginKey<any>;
|
|
213
404
|
/**
|
|
214
405
|
* This utility allows you to create suggestions.
|
|
215
406
|
* @see https://tiptap.dev/api/utilities/suggestion
|
|
216
407
|
*/
|
|
217
|
-
declare function Suggestion<I = any, TSelected = any>({ pluginKey, editor, char, allowSpaces, allowToIncludeChar, allowedPrefixes, startOfLine, decorationTag, decorationClass, decorationContent, decorationEmptyClass, command, items, render, allow, findSuggestionMatch, shouldShow, shouldResetDismissed, }: SuggestionOptions<I, TSelected>): Plugin<
|
|
408
|
+
declare function Suggestion<I = any, TSelected = any>({ pluginKey, editor, char, allowSpaces, allowToIncludeChar, allowedPrefixes, startOfLine, decorationTag, decorationClass, decorationContent, decorationEmptyClass, command, items, minQueryLength, debounce, initialItems, placement, offset: offsetOption, container, flip, floatingUi, dismissOnOutsideClick, render, allow, findSuggestionMatch, shouldShow, shouldResetDismissed, }: SuggestionOptions<I, TSelected>): Plugin<SuggestionPluginState>;
|
|
218
409
|
/**
|
|
219
410
|
* Programmatically exit a suggestion plugin by dispatching a metadata-only
|
|
220
411
|
* transaction. This is the safe, recommended API to remove suggestion
|
|
@@ -222,4 +413,4 @@ declare function Suggestion<I = any, TSelected = any>({ pluginKey, editor, char,
|
|
|
222
413
|
*/
|
|
223
414
|
declare function exitSuggestion(view: EditorView, pluginKeyRef?: PluginKey): void;
|
|
224
415
|
|
|
225
|
-
export { Suggestion, type SuggestionKeyDownProps, type SuggestionMatch, type SuggestionOptions, SuggestionPluginKey, type SuggestionProps, type Trigger, Suggestion as default, exitSuggestion, findSuggestionMatch };
|
|
416
|
+
export { Suggestion, type SuggestionFloatingUiConfig, type SuggestionFloatingUiOptions, type SuggestionKeyDownProps, type SuggestionMatch, type SuggestionMount, type SuggestionMountOptions, type SuggestionOptions, type SuggestionPlacement, SuggestionPluginKey, type SuggestionPositionData, type SuggestionProps, type Trigger, Suggestion as default, exitSuggestion, findSuggestionMatch };
|