@tiptap/suggestion 3.3.1 → 3.4.1
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 +19 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +19 -47
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/suggestion.ts +26 -66
package/dist/index.cjs
CHANGED
|
@@ -77,7 +77,6 @@ function findSuggestionMatch(config) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// src/suggestion.ts
|
|
80
|
-
var clickHandlerMap = /* @__PURE__ */ new WeakMap();
|
|
81
80
|
var SuggestionPluginKey = new import_state.PluginKey("suggestion");
|
|
82
81
|
function Suggestion({
|
|
83
82
|
pluginKey = SuggestionPluginKey,
|
|
@@ -99,9 +98,19 @@ function Suggestion({
|
|
|
99
98
|
}) {
|
|
100
99
|
let props;
|
|
101
100
|
const renderer = render == null ? void 0 : render();
|
|
101
|
+
const getAnchorClientRect = () => {
|
|
102
|
+
const pos = editor.state.selection.$anchor.pos;
|
|
103
|
+
const coords = editor.view.coordsAtPos(pos);
|
|
104
|
+
const { top, right, bottom, left } = coords;
|
|
105
|
+
try {
|
|
106
|
+
return new DOMRect(left, top, right - left, bottom - top);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
102
111
|
const clientRectFor = (view, decorationNode) => {
|
|
103
112
|
if (!decorationNode) {
|
|
104
|
-
return
|
|
113
|
+
return getAnchorClientRect;
|
|
105
114
|
}
|
|
106
115
|
return () => {
|
|
107
116
|
const state = pluginKey.getState(editor.state);
|
|
@@ -136,42 +145,7 @@ function Suggestion({
|
|
|
136
145
|
}
|
|
137
146
|
const plugin = new import_state.Plugin({
|
|
138
147
|
key: pluginKey,
|
|
139
|
-
view(
|
|
140
|
-
const ensureClickHandler = (view) => {
|
|
141
|
-
if (clickHandlerMap.has(view)) {
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const handler = (event) => {
|
|
145
|
-
if (!props) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
const decorationNode = props.decorationNode;
|
|
149
|
-
const target = event.target;
|
|
150
|
-
if (!decorationNode) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (target && decorationNode.contains(target)) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
if (target && view.dom.contains(target)) {
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
if (target && target.closest && target.closest(".react-renderer")) {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
dispatchExit(view, pluginKey);
|
|
163
|
-
};
|
|
164
|
-
document.addEventListener("mousedown", handler, true);
|
|
165
|
-
clickHandlerMap.set(view, handler);
|
|
166
|
-
};
|
|
167
|
-
const removeClickHandler = (view) => {
|
|
168
|
-
const handler = clickHandlerMap.get(view);
|
|
169
|
-
if (!handler) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
document.removeEventListener("mousedown", handler, true);
|
|
173
|
-
clickHandlerMap.delete(view);
|
|
174
|
-
};
|
|
148
|
+
view() {
|
|
175
149
|
return {
|
|
176
150
|
update: async (view, prevState) => {
|
|
177
151
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
@@ -226,15 +200,9 @@ function Suggestion({
|
|
|
226
200
|
if (handleStart) {
|
|
227
201
|
(_g = renderer == null ? void 0 : renderer.onStart) == null ? void 0 : _g.call(renderer, props);
|
|
228
202
|
}
|
|
229
|
-
if (next.active) {
|
|
230
|
-
ensureClickHandler(view);
|
|
231
|
-
} else {
|
|
232
|
-
removeClickHandler(editorView);
|
|
233
|
-
}
|
|
234
203
|
},
|
|
235
204
|
destroy: () => {
|
|
236
205
|
var _a;
|
|
237
|
-
removeClickHandler(editorView);
|
|
238
206
|
if (!props) {
|
|
239
207
|
return;
|
|
240
208
|
}
|
|
@@ -316,7 +284,7 @@ function Suggestion({
|
|
|
316
284
|
props: {
|
|
317
285
|
// Call the keydown hook if suggestion is active.
|
|
318
286
|
handleKeyDown(view, event) {
|
|
319
|
-
var _a, _b, _c;
|
|
287
|
+
var _a, _b, _c, _d;
|
|
320
288
|
const { active, range } = plugin.getState(view.state);
|
|
321
289
|
if (!active) {
|
|
322
290
|
return false;
|
|
@@ -325,6 +293,10 @@ function Suggestion({
|
|
|
325
293
|
const state = plugin.getState(view.state);
|
|
326
294
|
const cachedNode = (_a = props == null ? void 0 : props.decorationNode) != null ? _a : null;
|
|
327
295
|
const decorationNode = cachedNode != null ? cachedNode : (state == null ? void 0 : state.decorationId) ? view.dom.querySelector(`[data-decoration-id="${state.decorationId}"]`) : null;
|
|
296
|
+
const handledByKeyDown = ((_b = renderer == null ? void 0 : renderer.onKeyDown) == null ? void 0 : _b.call(renderer, { view, event, range: state.range })) || false;
|
|
297
|
+
if (handledByKeyDown) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
328
300
|
const exitProps = {
|
|
329
301
|
editor,
|
|
330
302
|
range: state.range,
|
|
@@ -342,11 +314,11 @@ function Suggestion({
|
|
|
342
314
|
return decorationNode.getBoundingClientRect() || null;
|
|
343
315
|
} : null
|
|
344
316
|
};
|
|
345
|
-
(
|
|
317
|
+
(_c = renderer == null ? void 0 : renderer.onExit) == null ? void 0 : _c.call(renderer, exitProps);
|
|
346
318
|
dispatchExit(view, pluginKey);
|
|
347
319
|
return true;
|
|
348
320
|
}
|
|
349
|
-
const handled = ((
|
|
321
|
+
const handled = ((_d = renderer == null ? void 0 : renderer.onKeyDown) == null ? void 0 : _d.call(renderer, { view, event, range })) || false;
|
|
350
322
|
return handled;
|
|
351
323
|
},
|
|
352
324
|
// Setup decorator on the currently active suggestion.
|
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 } 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 { findSuggestionMatch as defaultFindSuggestionMatch } from './findSuggestionMatch.js'\n\n// Track document click handlers per EditorView instance to avoid accidental\n// leaks when multiple editors are created/destroyed. WeakMap ensures handlers\n// don't keep views alive.\nconst clickHandlerMap: WeakMap<EditorView, (event: MouseEvent) => void> = new WeakMap()\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 * 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: { editor: Editor; state: EditorState; range: Range; isActive?: boolean }) => 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}: SuggestionOptions<I, TSelected>) {\n let props: SuggestionProps<I, TSelected> | undefined\n const renderer = render?.()\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 null\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 // small helper used internally by the view to dispatch an exit\n function dispatchExit(view: EditorView, pluginKeyRef: PluginKey) {\n try {\n // Try to call renderer.onExit so consumer renderers (for example the\n // demos' ReactRenderer) can clean up and unmount immediately. This\n // covers paths where we only dispatch a metadata transaction (like\n // click-outside) and ensures we don't leak DOM nodes / React roots.\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({ editor, range: state?.range || { from: 0, to: 0 }, props: commandProps as any })\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(editorView: EditorView) {\n const ensureClickHandler = (view: EditorView) => {\n if (clickHandlerMap.has(view)) {\n return\n }\n\n const handler = (event: MouseEvent) => {\n if (!props) {\n return\n }\n\n const decorationNode = props.decorationNode\n const target = event.target as Element | null\n\n if (!decorationNode) {\n return\n }\n\n if (target && decorationNode.contains(target)) {\n return\n }\n\n if (target && view.dom.contains(target)) {\n return\n }\n\n if (target && target.closest && target.closest('.react-renderer')) {\n return\n }\n\n dispatchExit(view, pluginKey)\n }\n\n document.addEventListener('mousedown', handler, true)\n clickHandlerMap.set(view, handler)\n }\n\n const removeClickHandler = (view: EditorView) => {\n const handler = clickHandlerMap.get(view)\n if (!handler) {\n return\n }\n\n document.removeEventListener('mousedown', handler, true)\n clickHandlerMap.delete(view)\n }\n\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(`[data-decoration-id=\"${state.decorationId}\"]`)\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 // Install / remove click handler depending on suggestion active state\n if (next.active) {\n ensureClickHandler(view)\n } else {\n removeClickHandler(editorView)\n }\n },\n\n destroy: () => {\n removeClickHandler(editorView)\n\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 } = {\n active: false,\n range: {\n from: 0,\n to: 0,\n },\n query: null,\n text: null,\n composing: false,\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\n return next\n }\n\n next.composing = composing\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 ) {\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 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 const cachedNode = props?.decorationNode ?? null\n const decorationNode =\n cachedNode ??\n (state?.decorationId ? view.dom.querySelector(`[data-decoration-id=\"${state.decorationId}\"]`) : null)\n\n const exitProps: SuggestionProps = {\n editor,\n range: state.range,\n query: state.query,\n text: state.text,\n items: [],\n command: commandProps => {\n return command({ editor, range: state.range, props: commandProps as any })\n },\n decorationNode,\n // If we have a cached decoration node, use it for the clientRect\n // to avoid another DOM lookup. If not, leave clientRect null and\n // let consumer decide if they want to query.\n clientRect: decorationNode\n ? () => {\n return decorationNode.getBoundingClientRect() || null\n }\n : null,\n }\n\n renderer?.onExit?.(exitProps)\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 { char, allowSpaces: allowSpacesOption, allowToIncludeChar, allowedPrefixes, startOfLine, $position } = 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"],"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 } 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 { findSuggestionMatch as defaultFindSuggestionMatch } from './findSuggestionMatch.js'\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 * 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: { editor: Editor; state: EditorState; range: Range; isActive?: boolean }) => 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}: SuggestionOptions<I, TSelected>) {\n let props: SuggestionProps<I, TSelected> | undefined\n const renderer = render?.()\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 // 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({ editor, range: state?.range || { from: 0, to: 0 }, props: commandProps as any })\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(`[data-decoration-id=\"${state.decorationId}\"]`)\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 } = {\n active: false,\n range: {\n from: 0,\n to: 0,\n },\n query: null,\n text: null,\n composing: false,\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\n return next\n }\n\n next.composing = composing\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 ) {\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 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 const cachedNode = props?.decorationNode ?? null\n const decorationNode =\n cachedNode ??\n (state?.decorationId ? view.dom.querySelector(`[data-decoration-id=\"${state.decorationId}\"]`) : null)\n\n // Give the consumer a chance to handle Escape via onKeyDown first.\n // If the consumer returns `true` we assume they handled the event and\n // we won't call onExit/dispatchExit so they can both prevent\n // propagation and decide whether to close the suggestion themselves.\n const handledByKeyDown = renderer?.onKeyDown?.({ view, event, range: state.range }) || false\n\n if (handledByKeyDown) {\n return true\n }\n\n const exitProps: SuggestionProps = {\n editor,\n range: state.range,\n query: state.query,\n text: state.text,\n items: [],\n command: commandProps => {\n return command({ editor, range: state.range, props: commandProps as any })\n },\n decorationNode,\n // If we have a cached decoration node, use it for the clientRect\n // to avoid another DOM lookup. If not, leave clientRect null and\n // let consumer decide if they want to query.\n clientRect: decorationNode\n ? () => {\n return decorationNode.getBoundingClientRect() || null\n }\n : null,\n }\n\n renderer?.onExit?.(exitProps)\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 { char, allowSpaces: allowSpacesOption, allowToIncludeChar, allowedPrefixes, startOfLine, $position } = 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"]}
|
package/dist/index.js
CHANGED
|
@@ -47,7 +47,6 @@ function findSuggestionMatch(config) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// src/suggestion.ts
|
|
50
|
-
var clickHandlerMap = /* @__PURE__ */ new WeakMap();
|
|
51
50
|
var SuggestionPluginKey = new PluginKey("suggestion");
|
|
52
51
|
function Suggestion({
|
|
53
52
|
pluginKey = SuggestionPluginKey,
|
|
@@ -69,9 +68,19 @@ function Suggestion({
|
|
|
69
68
|
}) {
|
|
70
69
|
let props;
|
|
71
70
|
const renderer = render == null ? void 0 : render();
|
|
71
|
+
const getAnchorClientRect = () => {
|
|
72
|
+
const pos = editor.state.selection.$anchor.pos;
|
|
73
|
+
const coords = editor.view.coordsAtPos(pos);
|
|
74
|
+
const { top, right, bottom, left } = coords;
|
|
75
|
+
try {
|
|
76
|
+
return new DOMRect(left, top, right - left, bottom - top);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
72
81
|
const clientRectFor = (view, decorationNode) => {
|
|
73
82
|
if (!decorationNode) {
|
|
74
|
-
return
|
|
83
|
+
return getAnchorClientRect;
|
|
75
84
|
}
|
|
76
85
|
return () => {
|
|
77
86
|
const state = pluginKey.getState(editor.state);
|
|
@@ -106,42 +115,7 @@ function Suggestion({
|
|
|
106
115
|
}
|
|
107
116
|
const plugin = new Plugin({
|
|
108
117
|
key: pluginKey,
|
|
109
|
-
view(
|
|
110
|
-
const ensureClickHandler = (view) => {
|
|
111
|
-
if (clickHandlerMap.has(view)) {
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
const handler = (event) => {
|
|
115
|
-
if (!props) {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const decorationNode = props.decorationNode;
|
|
119
|
-
const target = event.target;
|
|
120
|
-
if (!decorationNode) {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
if (target && decorationNode.contains(target)) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (target && view.dom.contains(target)) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (target && target.closest && target.closest(".react-renderer")) {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
dispatchExit(view, pluginKey);
|
|
133
|
-
};
|
|
134
|
-
document.addEventListener("mousedown", handler, true);
|
|
135
|
-
clickHandlerMap.set(view, handler);
|
|
136
|
-
};
|
|
137
|
-
const removeClickHandler = (view) => {
|
|
138
|
-
const handler = clickHandlerMap.get(view);
|
|
139
|
-
if (!handler) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
document.removeEventListener("mousedown", handler, true);
|
|
143
|
-
clickHandlerMap.delete(view);
|
|
144
|
-
};
|
|
118
|
+
view() {
|
|
145
119
|
return {
|
|
146
120
|
update: async (view, prevState) => {
|
|
147
121
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
@@ -196,15 +170,9 @@ function Suggestion({
|
|
|
196
170
|
if (handleStart) {
|
|
197
171
|
(_g = renderer == null ? void 0 : renderer.onStart) == null ? void 0 : _g.call(renderer, props);
|
|
198
172
|
}
|
|
199
|
-
if (next.active) {
|
|
200
|
-
ensureClickHandler(view);
|
|
201
|
-
} else {
|
|
202
|
-
removeClickHandler(editorView);
|
|
203
|
-
}
|
|
204
173
|
},
|
|
205
174
|
destroy: () => {
|
|
206
175
|
var _a;
|
|
207
|
-
removeClickHandler(editorView);
|
|
208
176
|
if (!props) {
|
|
209
177
|
return;
|
|
210
178
|
}
|
|
@@ -286,7 +254,7 @@ function Suggestion({
|
|
|
286
254
|
props: {
|
|
287
255
|
// Call the keydown hook if suggestion is active.
|
|
288
256
|
handleKeyDown(view, event) {
|
|
289
|
-
var _a, _b, _c;
|
|
257
|
+
var _a, _b, _c, _d;
|
|
290
258
|
const { active, range } = plugin.getState(view.state);
|
|
291
259
|
if (!active) {
|
|
292
260
|
return false;
|
|
@@ -295,6 +263,10 @@ function Suggestion({
|
|
|
295
263
|
const state = plugin.getState(view.state);
|
|
296
264
|
const cachedNode = (_a = props == null ? void 0 : props.decorationNode) != null ? _a : null;
|
|
297
265
|
const decorationNode = cachedNode != null ? cachedNode : (state == null ? void 0 : state.decorationId) ? view.dom.querySelector(`[data-decoration-id="${state.decorationId}"]`) : null;
|
|
266
|
+
const handledByKeyDown = ((_b = renderer == null ? void 0 : renderer.onKeyDown) == null ? void 0 : _b.call(renderer, { view, event, range: state.range })) || false;
|
|
267
|
+
if (handledByKeyDown) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
298
270
|
const exitProps = {
|
|
299
271
|
editor,
|
|
300
272
|
range: state.range,
|
|
@@ -312,11 +284,11 @@ function Suggestion({
|
|
|
312
284
|
return decorationNode.getBoundingClientRect() || null;
|
|
313
285
|
} : null
|
|
314
286
|
};
|
|
315
|
-
(
|
|
287
|
+
(_c = renderer == null ? void 0 : renderer.onExit) == null ? void 0 : _c.call(renderer, exitProps);
|
|
316
288
|
dispatchExit(view, pluginKey);
|
|
317
289
|
return true;
|
|
318
290
|
}
|
|
319
|
-
const handled = ((
|
|
291
|
+
const handled = ((_d = renderer == null ? void 0 : renderer.onKeyDown) == null ? void 0 : _d.call(renderer, { view, event, range })) || false;
|
|
320
292
|
return handled;
|
|
321
293
|
},
|
|
322
294
|
// Setup decorator on the currently active suggestion.
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/suggestion.ts","../src/findSuggestionMatch.ts","../src/index.ts"],"sourcesContent":["import type { Editor, Range } from '@tiptap/core'\nimport type { EditorState } 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 { findSuggestionMatch as defaultFindSuggestionMatch } from './findSuggestionMatch.js'\n\n// Track document click handlers per EditorView instance to avoid accidental\n// leaks when multiple editors are created/destroyed. WeakMap ensures handlers\n// don't keep views alive.\nconst clickHandlerMap: WeakMap<EditorView, (event: MouseEvent) => void> = new WeakMap()\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 * 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: { editor: Editor; state: EditorState; range: Range; isActive?: boolean }) => 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}: SuggestionOptions<I, TSelected>) {\n let props: SuggestionProps<I, TSelected> | undefined\n const renderer = render?.()\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 null\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 // small helper used internally by the view to dispatch an exit\n function dispatchExit(view: EditorView, pluginKeyRef: PluginKey) {\n try {\n // Try to call renderer.onExit so consumer renderers (for example the\n // demos' ReactRenderer) can clean up and unmount immediately. This\n // covers paths where we only dispatch a metadata transaction (like\n // click-outside) and ensures we don't leak DOM nodes / React roots.\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({ editor, range: state?.range || { from: 0, to: 0 }, props: commandProps as any })\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(editorView: EditorView) {\n const ensureClickHandler = (view: EditorView) => {\n if (clickHandlerMap.has(view)) {\n return\n }\n\n const handler = (event: MouseEvent) => {\n if (!props) {\n return\n }\n\n const decorationNode = props.decorationNode\n const target = event.target as Element | null\n\n if (!decorationNode) {\n return\n }\n\n if (target && decorationNode.contains(target)) {\n return\n }\n\n if (target && view.dom.contains(target)) {\n return\n }\n\n if (target && target.closest && target.closest('.react-renderer')) {\n return\n }\n\n dispatchExit(view, pluginKey)\n }\n\n document.addEventListener('mousedown', handler, true)\n clickHandlerMap.set(view, handler)\n }\n\n const removeClickHandler = (view: EditorView) => {\n const handler = clickHandlerMap.get(view)\n if (!handler) {\n return\n }\n\n document.removeEventListener('mousedown', handler, true)\n clickHandlerMap.delete(view)\n }\n\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(`[data-decoration-id=\"${state.decorationId}\"]`)\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 // Install / remove click handler depending on suggestion active state\n if (next.active) {\n ensureClickHandler(view)\n } else {\n removeClickHandler(editorView)\n }\n },\n\n destroy: () => {\n removeClickHandler(editorView)\n\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 } = {\n active: false,\n range: {\n from: 0,\n to: 0,\n },\n query: null,\n text: null,\n composing: false,\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\n return next\n }\n\n next.composing = composing\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 ) {\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 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 const cachedNode = props?.decorationNode ?? null\n const decorationNode =\n cachedNode ??\n (state?.decorationId ? view.dom.querySelector(`[data-decoration-id=\"${state.decorationId}\"]`) : null)\n\n const exitProps: SuggestionProps = {\n editor,\n range: state.range,\n query: state.query,\n text: state.text,\n items: [],\n command: commandProps => {\n return command({ editor, range: state.range, props: commandProps as any })\n },\n decorationNode,\n // If we have a cached decoration node, use it for the clientRect\n // to avoid another DOM lookup. If not, leave clientRect null and\n // let consumer decide if they want to query.\n clientRect: decorationNode\n ? () => {\n return decorationNode.getBoundingClientRect() || null\n }\n : null,\n }\n\n renderer?.onExit?.(exitProps)\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 { char, allowSpaces: allowSpacesOption, allowToIncludeChar, allowedPrefixes, startOfLine, $position } = 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 { exitSuggestion, Suggestion } from './suggestion.js'\n\nexport * from './findSuggestionMatch.js'\nexport * from './suggestion.js'\n\nexport { exitSuggestion }\n\nexport default Suggestion\n"],"mappings":";AAEA,SAAS,QAAQ,iBAAiB;AAElC,SAAS,YAAY,qBAAqB;;;ACH1C,SAAS,sBAAsB;AAkBxB,SAAS,oBAAoB,QAAkC;AAnBtE;AAoBE,QAAM,EAAE,MAAM,aAAa,mBAAmB,oBAAoB,iBAAiB,aAAa,UAAU,IAAI;AAE9G,QAAM,cAAc,qBAAqB,CAAC;AAE1C,QAAM,cAAc,eAAe,IAAI;AACvC,QAAM,SAAS,IAAI,OAAO,MAAM,WAAW,GAAG;AAC9C,QAAM,SAAS,cAAc,MAAM;AACnC,QAAM,mBAAmB,qBAAqB,KAAK;AACnD,QAAM,SAAS,cACX,IAAI,OAAO,GAAG,MAAM,GAAG,WAAW,YAAY,gBAAgB,OAAO,IAAI,IACzE,IAAI,OAAO,GAAG,MAAM,SAAS,WAAW,QAAQ,gBAAgB,MAAM,IAAI;AAE9E,QAAM,SAAO,eAAU,eAAV,mBAAsB,WAAU,UAAU,WAAW;AAElE,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,UAAU,MAAM,KAAK;AACtC,QAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,MAAM,CAAC,EAAE,IAAI;AAEpD,MAAI,CAAC,SAAS,MAAM,UAAU,UAAa,MAAM,UAAU,QAAW;AACpE,WAAO;AAAA,EACT;AAIA,QAAM,cAAc,MAAM,MAAM,MAAM,KAAK,IAAI,GAAG,MAAM,QAAQ,CAAC,GAAG,MAAM,KAAK;AAC/E,QAAM,uBAAuB,IAAI,OAAO,KAAK,mDAAiB,KAAK,GAAG,OAAO,EAAE,KAAK,WAAW;AAE/F,MAAI,oBAAoB,QAAQ,CAAC,sBAAsB;AACrD,WAAO;AAAA,EACT;AAGA,QAAM,OAAO,WAAW,MAAM;AAC9B,MAAI,KAAK,OAAO,MAAM,CAAC,EAAE;AAIzB,MAAI,eAAe,OAAO,KAAK,KAAK,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,GAAG;AAC1D,UAAM,CAAC,KAAK;AACZ,UAAM;AAAA,EACR;AAGA,MAAI,OAAO,UAAU,OAAO,MAAM,UAAU,KAAK;AAC/C,WAAO;AAAA,MACL,OAAO;AAAA,QACL;AAAA,QACA;AAAA,MACF;AAAA,MACA,OAAO,MAAM,CAAC,EAAE,MAAM,KAAK,MAAM;AAAA,MACjC,MAAM,MAAM,CAAC;AAAA,IACf;AAAA,EACF;AAEA,SAAO;AACT;;;ADnEA,IAAM,kBAAoE,oBAAI,QAAQ;AA8K/E,IAAM,sBAAsB,IAAI,UAAU,YAAY;AAMtD,SAAS,WAAqC;AAAA,EACnD,YAAY;AAAA,EACZ;AAAA,EACA,OAAO;AAAA,EACP,cAAc;AAAA,EACd,qBAAqB;AAAA,EACrB,kBAAkB,CAAC,GAAG;AAAA,EACtB,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,uBAAuB;AAAA,EACvB,UAAU,MAAM;AAAA,EAChB,QAAQ,MAAM,CAAC;AAAA,EACf,SAAS,OAAO,CAAC;AAAA,EACjB,QAAQ,MAAM;AAAA,EACd,qBAAAA,uBAAsB;AACxB,GAAoC;AAClC,MAAI;AACJ,QAAM,WAAW;AAMjB,QAAM,gBAAgB,CAAC,MAAkB,mBAAmC;AAC1E,QAAI,CAAC,gBAAgB;AACnB,aAAO;AAAA,IACT;AAEA,WAAO,MAAM;AACX,YAAM,QAAQ,UAAU,SAAS,OAAO,KAAK;AAC7C,YAAM,eAAe,+BAAO;AAC5B,YAAM,wBAAwB,KAAK,IAAI,cAAc,wBAAwB,YAAY,IAAI;AAE7F,cAAO,+DAAuB,4BAA2B;AAAA,IAC3D;AAAA,EACF;AAEA,WAAS,aAAa,MAAkB,cAAyB;AAtOnE;AAuOI,QAAI;AAKF,YAAM,QAAQ,UAAU,SAAS,KAAK,KAAK;AAC3C,YAAM,kBAAiB,+BAAO,gBAC1B,KAAK,IAAI,cAAc,wBAAwB,MAAM,YAAY,IAAI,IACrE;AAEJ,YAAM,YAA6B;AAAA;AAAA,QAEjC;AAAA,QACA,QAAO,+BAAO,UAAS,EAAE,MAAM,GAAG,IAAI,EAAE;AAAA,QACxC,QAAO,+BAAO,UAAS;AAAA,QACvB,OAAM,+BAAO,SAAQ;AAAA,QACrB,OAAO,CAAC;AAAA,QACR,SAAS,kBAAgB;AACvB,iBAAO,QAAQ,EAAE,QAAQ,QAAO,+BAAO,UAAS,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,OAAO,aAAoB,CAAC;AAAA,QAClG;AAAA,QACA;AAAA,QACA,YAAY,cAAc,MAAM,cAAc;AAAA,MAChD;AAEA,iDAAU,WAAV,kCAAmB;AAAA,IACrB,QAAQ;AAAA,IAER;AAEA,UAAM,KAAK,KAAK,MAAM,GAAG,QAAQ,cAAc,EAAE,MAAM,KAAK,CAAC;AAE7D,SAAK,SAAS,EAAE;AAAA,EAClB;AAEA,QAAM,SAAsB,IAAI,OAAO;AAAA,IACrC,KAAK;AAAA,IAEL,KAAK,YAAwB;AAC3B,YAAM,qBAAqB,CAAC,SAAqB;AAC/C,YAAI,gBAAgB,IAAI,IAAI,GAAG;AAC7B;AAAA,QACF;AAEA,cAAM,UAAU,CAAC,UAAsB;AACrC,cAAI,CAAC,OAAO;AACV;AAAA,UACF;AAEA,gBAAM,iBAAiB,MAAM;AAC7B,gBAAM,SAAS,MAAM;AAErB,cAAI,CAAC,gBAAgB;AACnB;AAAA,UACF;AAEA,cAAI,UAAU,eAAe,SAAS,MAAM,GAAG;AAC7C;AAAA,UACF;AAEA,cAAI,UAAU,KAAK,IAAI,SAAS,MAAM,GAAG;AACvC;AAAA,UACF;AAEA,cAAI,UAAU,OAAO,WAAW,OAAO,QAAQ,iBAAiB,GAAG;AACjE;AAAA,UACF;AAEA,uBAAa,MAAM,SAAS;AAAA,QAC9B;AAEA,iBAAS,iBAAiB,aAAa,SAAS,IAAI;AACpD,wBAAgB,IAAI,MAAM,OAAO;AAAA,MACnC;AAEA,YAAM,qBAAqB,CAAC,SAAqB;AAC/C,cAAM,UAAU,gBAAgB,IAAI,IAAI;AACxC,YAAI,CAAC,SAAS;AACZ;AAAA,QACF;AAEA,iBAAS,oBAAoB,aAAa,SAAS,IAAI;AACvD,wBAAgB,OAAO,IAAI;AAAA,MAC7B;AAEA,aAAO;AAAA,QACL,QAAQ,OAAO,MAAM,cAAc;AA5T3C;AA6TU,gBAAM,QAAO,UAAK,QAAL,mBAAU,SAAS;AAChC,gBAAM,QAAO,UAAK,QAAL,mBAAU,SAAS,KAAK;AAGrC,gBAAM,QAAQ,KAAK,UAAU,KAAK,UAAU,KAAK,MAAM,SAAS,KAAK,MAAM;AAC3E,gBAAM,UAAU,CAAC,KAAK,UAAU,KAAK;AACrC,gBAAM,UAAU,KAAK,UAAU,CAAC,KAAK;AACrC,gBAAM,UAAU,CAAC,WAAW,CAAC,WAAW,KAAK,UAAU,KAAK;AAE5D,gBAAM,cAAc,WAAY,SAAS;AACzC,gBAAM,eAAe,WAAW;AAChC,gBAAM,aAAa,WAAY,SAAS;AAGxC,cAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,YAAY;AAChD;AAAA,UACF;AAEA,gBAAM,QAAQ,cAAc,CAAC,cAAc,OAAO;AAClD,gBAAM,iBAAiB,KAAK,IAAI,cAAc,wBAAwB,MAAM,YAAY,IAAI;AAE5F,kBAAQ;AAAA,YACN;AAAA,YACA,OAAO,MAAM;AAAA,YACb,OAAO,MAAM;AAAA,YACb,MAAM,MAAM;AAAA,YACZ,OAAO,CAAC;AAAA,YACR,SAAS,kBAAgB;AACvB,qBAAO,QAAQ;AAAA,gBACb;AAAA,gBACA,OAAO,MAAM;AAAA,gBACb,OAAO;AAAA,cACT,CAAC;AAAA,YACH;AAAA,YACA;AAAA,YACA,YAAY,cAAc,MAAM,cAAc;AAAA,UAChD;AAEA,cAAI,aAAa;AACf,uDAAU,kBAAV,kCAA0B;AAAA,UAC5B;AAEA,cAAI,cAAc;AAChB,uDAAU,mBAAV,kCAA2B;AAAA,UAC7B;AAEA,cAAI,gBAAgB,aAAa;AAC/B,kBAAM,QAAQ,MAAM,MAAM;AAAA,cACxB;AAAA,cACA,OAAO,MAAM;AAAA,YACf,CAAC;AAAA,UACH;AAEA,cAAI,YAAY;AACd,uDAAU,WAAV,kCAAmB;AAAA,UACrB;AAEA,cAAI,cAAc;AAChB,uDAAU,aAAV,kCAAqB;AAAA,UACvB;AAEA,cAAI,aAAa;AACf,uDAAU,YAAV,kCAAoB;AAAA,UACtB;AAGA,cAAI,KAAK,QAAQ;AACf,+BAAmB,IAAI;AAAA,UACzB,OAAO;AACL,+BAAmB,UAAU;AAAA,UAC/B;AAAA,QACF;AAAA,QAEA,SAAS,MAAM;AAtYvB;AAuYU,6BAAmB,UAAU;AAE7B,cAAI,CAAC,OAAO;AACV;AAAA,UACF;AAEA,qDAAU,WAAV,kCAAmB;AAAA,QACrB;AAAA,MACF;AAAA,IACF;AAAA,IAEA,OAAO;AAAA;AAAA,MAEL,OAAO;AACL,cAAM,QAOF;AAAA,UACF,QAAQ;AAAA,UACR,OAAO;AAAA,YACL,MAAM;AAAA,YACN,IAAI;AAAA,UACN;AAAA,UACA,OAAO;AAAA,UACP,MAAM;AAAA,UACN,WAAW;AAAA,QACb;AAEA,eAAO;AAAA,MACT;AAAA;AAAA,MAGA,MAAM,aAAa,MAAM,WAAW,OAAO;AACzC,cAAM,EAAE,WAAW,IAAI;AACvB,cAAM,EAAE,UAAU,IAAI,OAAO;AAC7B,cAAM,EAAE,UAAU,IAAI;AACtB,cAAM,EAAE,OAAO,KAAK,IAAI;AACxB,cAAM,OAAO,EAAE,GAAG,KAAK;AAMvB,cAAM,OAAO,YAAY,QAAQ,SAAS;AAC1C,YAAI,QAAQ,KAAK,MAAM;AACrB,eAAK,SAAS;AACd,eAAK,eAAe;AACpB,eAAK,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE;AAC9B,eAAK,QAAQ;AACb,eAAK,OAAO;AAEZ,iBAAO;AAAA,QACT;AAEA,aAAK,YAAY;AAKjB,YAAI,eAAe,SAAS,OAAO,KAAK,YAAY;AAElD,eAAK,OAAO,KAAK,MAAM,QAAQ,OAAO,KAAK,MAAM,OAAO,CAAC,aAAa,CAAC,KAAK,WAAW;AACrF,iBAAK,SAAS;AAAA,UAChB;AAGA,gBAAM,QAAQA,qBAAoB;AAAA,YAChC;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,WAAW,UAAU;AAAA,UACvB,CAAC;AACD,gBAAM,eAAe,MAAM,KAAK,MAAM,KAAK,OAAO,IAAI,UAAU,CAAC;AAGjE,cACE,SACA,MAAM;AAAA,YACJ;AAAA,YACA;AAAA,YACA,OAAO,MAAM;AAAA,YACb,UAAU,KAAK;AAAA,UACjB,CAAC,GACD;AACA,iBAAK,SAAS;AACd,iBAAK,eAAe,KAAK,eAAe,KAAK,eAAe;AAC5D,iBAAK,QAAQ,MAAM;AACnB,iBAAK,QAAQ,MAAM;AACnB,iBAAK,OAAO,MAAM;AAAA,UACpB,OAAO;AACL,iBAAK,SAAS;AAAA,UAChB;AAAA,QACF,OAAO;AACL,eAAK,SAAS;AAAA,QAChB;AAGA,YAAI,CAAC,KAAK,QAAQ;AAChB,eAAK,eAAe;AACpB,eAAK,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE;AAC9B,eAAK,QAAQ;AACb,eAAK,OAAO;AAAA,QACd;AAEA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,OAAO;AAAA;AAAA,MAEL,cAAc,MAAM,OAAO;AA3fjC;AA4fQ,cAAM,EAAE,QAAQ,MAAM,IAAI,OAAO,SAAS,KAAK,KAAK;AAEpD,YAAI,CAAC,QAAQ;AACX,iBAAO;AAAA,QACT;AAMA,YAAI,MAAM,QAAQ,YAAY,MAAM,QAAQ,OAAO;AACjD,gBAAM,QAAQ,OAAO,SAAS,KAAK,KAAK;AACxC,gBAAM,cAAa,oCAAO,mBAAP,YAAyB;AAC5C,gBAAM,iBACJ,mCACC,+BAAO,gBAAe,KAAK,IAAI,cAAc,wBAAwB,MAAM,YAAY,IAAI,IAAI;AAElG,gBAAM,YAA6B;AAAA,YACjC;AAAA,YACA,OAAO,MAAM;AAAA,YACb,OAAO,MAAM;AAAA,YACb,MAAM,MAAM;AAAA,YACZ,OAAO,CAAC;AAAA,YACR,SAAS,kBAAgB;AACvB,qBAAO,QAAQ,EAAE,QAAQ,OAAO,MAAM,OAAO,OAAO,aAAoB,CAAC;AAAA,YAC3E;AAAA,YACA;AAAA;AAAA;AAAA;AAAA,YAIA,YAAY,iBACR,MAAM;AACJ,qBAAO,eAAe,sBAAsB,KAAK;AAAA,YACnD,IACA;AAAA,UACN;AAEA,qDAAU,WAAV,kCAAmB;AAGnB,uBAAa,MAAM,SAAS;AAE5B,iBAAO;AAAA,QACT;AAEA,cAAM,YAAU,0CAAU,cAAV,kCAAsB,EAAE,MAAM,OAAO,MAAM,OAAM;AACjE,eAAO;AAAA,MACT;AAAA;AAAA,MAGA,YAAY,OAAO;AACjB,cAAM,EAAE,QAAQ,OAAO,cAAc,MAAM,IAAI,OAAO,SAAS,KAAK;AAEpE,YAAI,CAAC,QAAQ;AACX,iBAAO;AAAA,QACT;AAEA,cAAM,UAAU,EAAC,+BAAO;AACxB,cAAM,aAAa,CAAC,eAAe;AAEnC,YAAI,SAAS;AACX,qBAAW,KAAK,oBAAoB;AAAA,QACtC;AAEA,eAAO,cAAc,OAAO,MAAM,KAAK;AAAA,UACrC,WAAW,OAAO,MAAM,MAAM,MAAM,IAAI;AAAA,YACtC,UAAU;AAAA,YACV,OAAO,WAAW,KAAK,GAAG;AAAA,YAC1B,sBAAsB;AAAA,YACtB,2BAA2B;AAAA,UAC7B,CAAC;AAAA,QACH,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,CAAC;AAED,SAAO;AACT;AAOO,SAAS,eAAe,MAAkB,eAA0B,qBAAqB;AAC9F,QAAM,KAAK,KAAK,MAAM,GAAG,QAAQ,cAAc,EAAE,MAAM,KAAK,CAAC;AAC7D,OAAK,SAAS,EAAE;AAClB;;;AE5kBA,IAAO,gBAAQ;","names":["findSuggestionMatch"]}
|
|
1
|
+
{"version":3,"sources":["../src/suggestion.ts","../src/findSuggestionMatch.ts","../src/index.ts"],"sourcesContent":["import type { Editor, Range } from '@tiptap/core'\nimport type { EditorState } 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 { findSuggestionMatch as defaultFindSuggestionMatch } from './findSuggestionMatch.js'\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 * 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: { editor: Editor; state: EditorState; range: Range; isActive?: boolean }) => 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}: SuggestionOptions<I, TSelected>) {\n let props: SuggestionProps<I, TSelected> | undefined\n const renderer = render?.()\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 // 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({ editor, range: state?.range || { from: 0, to: 0 }, props: commandProps as any })\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(`[data-decoration-id=\"${state.decorationId}\"]`)\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 } = {\n active: false,\n range: {\n from: 0,\n to: 0,\n },\n query: null,\n text: null,\n composing: false,\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\n return next\n }\n\n next.composing = composing\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 ) {\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 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 const cachedNode = props?.decorationNode ?? null\n const decorationNode =\n cachedNode ??\n (state?.decorationId ? view.dom.querySelector(`[data-decoration-id=\"${state.decorationId}\"]`) : null)\n\n // Give the consumer a chance to handle Escape via onKeyDown first.\n // If the consumer returns `true` we assume they handled the event and\n // we won't call onExit/dispatchExit so they can both prevent\n // propagation and decide whether to close the suggestion themselves.\n const handledByKeyDown = renderer?.onKeyDown?.({ view, event, range: state.range }) || false\n\n if (handledByKeyDown) {\n return true\n }\n\n const exitProps: SuggestionProps = {\n editor,\n range: state.range,\n query: state.query,\n text: state.text,\n items: [],\n command: commandProps => {\n return command({ editor, range: state.range, props: commandProps as any })\n },\n decorationNode,\n // If we have a cached decoration node, use it for the clientRect\n // to avoid another DOM lookup. If not, leave clientRect null and\n // let consumer decide if they want to query.\n clientRect: decorationNode\n ? () => {\n return decorationNode.getBoundingClientRect() || null\n }\n : null,\n }\n\n renderer?.onExit?.(exitProps)\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 { char, allowSpaces: allowSpacesOption, allowToIncludeChar, allowedPrefixes, startOfLine, $position } = 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 { exitSuggestion, Suggestion } from './suggestion.js'\n\nexport * from './findSuggestionMatch.js'\nexport * from './suggestion.js'\n\nexport { exitSuggestion }\n\nexport default Suggestion\n"],"mappings":"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","names":["findSuggestionMatch"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiptap/suggestion",
|
|
3
3
|
"description": "suggestion plugin for tiptap",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.4.1",
|
|
5
5
|
"homepage": "https://tiptap.dev",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"tiptap",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"dist"
|
|
32
32
|
],
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@tiptap/core": "^3.
|
|
35
|
-
"@tiptap/pm": "^3.
|
|
34
|
+
"@tiptap/core": "^3.4.1",
|
|
35
|
+
"@tiptap/pm": "^3.4.1"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@tiptap/core": "^3.
|
|
39
|
-
"@tiptap/pm": "^3.
|
|
38
|
+
"@tiptap/core": "^3.4.1",
|
|
39
|
+
"@tiptap/pm": "^3.4.1"
|
|
40
40
|
},
|
|
41
41
|
"repository": {
|
|
42
42
|
"type": "git",
|
package/src/suggestion.ts
CHANGED
|
@@ -6,11 +6,6 @@ import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
|
|
6
6
|
|
|
7
7
|
import { findSuggestionMatch as defaultFindSuggestionMatch } from './findSuggestionMatch.js'
|
|
8
8
|
|
|
9
|
-
// Track document click handlers per EditorView instance to avoid accidental
|
|
10
|
-
// leaks when multiple editors are created/destroyed. WeakMap ensures handlers
|
|
11
|
-
// don't keep views alive.
|
|
12
|
-
const clickHandlerMap: WeakMap<EditorView, (event: MouseEvent) => void> = new WeakMap()
|
|
13
|
-
|
|
14
9
|
export interface SuggestionOptions<I = any, TSelected = any> {
|
|
15
10
|
/**
|
|
16
11
|
* The plugin key for the suggestion plugin.
|
|
@@ -210,13 +205,27 @@ export function Suggestion<I = any, TSelected = any>({
|
|
|
210
205
|
let props: SuggestionProps<I, TSelected> | undefined
|
|
211
206
|
const renderer = render?.()
|
|
212
207
|
|
|
208
|
+
// Gets the DOM rectangle corresponding to the current editor cursor anchor position
|
|
209
|
+
// Calculates screen coordinates based on Tiptap's cursor position and converts to a DOMRect object
|
|
210
|
+
const getAnchorClientRect = () => {
|
|
211
|
+
const pos = editor.state.selection.$anchor.pos
|
|
212
|
+
const coords = editor.view.coordsAtPos(pos)
|
|
213
|
+
const { top, right, bottom, left } = coords
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
return new DOMRect(left, top, right - left, bottom - top)
|
|
217
|
+
} catch {
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
213
222
|
// Helper to create a clientRect callback for a given decoration node.
|
|
214
223
|
// Returns null when no decoration node is present. Uses the pluginKey's
|
|
215
224
|
// state to resolve the current decoration node on demand, avoiding a
|
|
216
225
|
// duplicated implementation in multiple places.
|
|
217
226
|
const clientRectFor = (view: EditorView, decorationNode: Element | null) => {
|
|
218
227
|
if (!decorationNode) {
|
|
219
|
-
return
|
|
228
|
+
return getAnchorClientRect
|
|
220
229
|
}
|
|
221
230
|
|
|
222
231
|
return () => {
|
|
@@ -230,10 +239,6 @@ export function Suggestion<I = any, TSelected = any>({
|
|
|
230
239
|
// small helper used internally by the view to dispatch an exit
|
|
231
240
|
function dispatchExit(view: EditorView, pluginKeyRef: PluginKey) {
|
|
232
241
|
try {
|
|
233
|
-
// Try to call renderer.onExit so consumer renderers (for example the
|
|
234
|
-
// demos' ReactRenderer) can clean up and unmount immediately. This
|
|
235
|
-
// covers paths where we only dispatch a metadata transaction (like
|
|
236
|
-
// click-outside) and ensures we don't leak DOM nodes / React roots.
|
|
237
242
|
const state = pluginKey.getState(view.state)
|
|
238
243
|
const decorationNode = state?.decorationId
|
|
239
244
|
? view.dom.querySelector(`[data-decoration-id="${state.decorationId}"]`)
|
|
@@ -266,53 +271,7 @@ export function Suggestion<I = any, TSelected = any>({
|
|
|
266
271
|
const plugin: Plugin<any> = new Plugin({
|
|
267
272
|
key: pluginKey,
|
|
268
273
|
|
|
269
|
-
view(
|
|
270
|
-
const ensureClickHandler = (view: EditorView) => {
|
|
271
|
-
if (clickHandlerMap.has(view)) {
|
|
272
|
-
return
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const handler = (event: MouseEvent) => {
|
|
276
|
-
if (!props) {
|
|
277
|
-
return
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const decorationNode = props.decorationNode
|
|
281
|
-
const target = event.target as Element | null
|
|
282
|
-
|
|
283
|
-
if (!decorationNode) {
|
|
284
|
-
return
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (target && decorationNode.contains(target)) {
|
|
288
|
-
return
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (target && view.dom.contains(target)) {
|
|
292
|
-
return
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (target && target.closest && target.closest('.react-renderer')) {
|
|
296
|
-
return
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
dispatchExit(view, pluginKey)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
document.addEventListener('mousedown', handler, true)
|
|
303
|
-
clickHandlerMap.set(view, handler)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const removeClickHandler = (view: EditorView) => {
|
|
307
|
-
const handler = clickHandlerMap.get(view)
|
|
308
|
-
if (!handler) {
|
|
309
|
-
return
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
document.removeEventListener('mousedown', handler, true)
|
|
313
|
-
clickHandlerMap.delete(view)
|
|
314
|
-
}
|
|
315
|
-
|
|
274
|
+
view() {
|
|
316
275
|
return {
|
|
317
276
|
update: async (view, prevState) => {
|
|
318
277
|
const prev = this.key?.getState(prevState)
|
|
@@ -379,18 +338,9 @@ export function Suggestion<I = any, TSelected = any>({
|
|
|
379
338
|
if (handleStart) {
|
|
380
339
|
renderer?.onStart?.(props)
|
|
381
340
|
}
|
|
382
|
-
|
|
383
|
-
// Install / remove click handler depending on suggestion active state
|
|
384
|
-
if (next.active) {
|
|
385
|
-
ensureClickHandler(view)
|
|
386
|
-
} else {
|
|
387
|
-
removeClickHandler(editorView)
|
|
388
|
-
}
|
|
389
341
|
},
|
|
390
342
|
|
|
391
343
|
destroy: () => {
|
|
392
|
-
removeClickHandler(editorView)
|
|
393
|
-
|
|
394
344
|
if (!props) {
|
|
395
345
|
return
|
|
396
346
|
}
|
|
@@ -523,6 +473,16 @@ export function Suggestion<I = any, TSelected = any>({
|
|
|
523
473
|
cachedNode ??
|
|
524
474
|
(state?.decorationId ? view.dom.querySelector(`[data-decoration-id="${state.decorationId}"]`) : null)
|
|
525
475
|
|
|
476
|
+
// Give the consumer a chance to handle Escape via onKeyDown first.
|
|
477
|
+
// If the consumer returns `true` we assume they handled the event and
|
|
478
|
+
// we won't call onExit/dispatchExit so they can both prevent
|
|
479
|
+
// propagation and decide whether to close the suggestion themselves.
|
|
480
|
+
const handledByKeyDown = renderer?.onKeyDown?.({ view, event, range: state.range }) || false
|
|
481
|
+
|
|
482
|
+
if (handledByKeyDown) {
|
|
483
|
+
return true
|
|
484
|
+
}
|
|
485
|
+
|
|
526
486
|
const exitProps: SuggestionProps = {
|
|
527
487
|
editor,
|
|
528
488
|
range: state.range,
|