Socket
Book a DemoInstallSign in
Socket

@tiptap/suggestion

Package Overview
Dependencies
Maintainers
5
Versions
315
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tiptap/suggestion - npm Package Compare versions

Comparing version

to
3.3.0

8

dist/index.d.ts

@@ -187,3 +187,9 @@ import { Range, Editor } from '@tiptap/core';

declare function Suggestion<I = any, TSelected = any>({ pluginKey, editor, char, allowSpaces, allowToIncludeChar, allowedPrefixes, startOfLine, decorationTag, decorationClass, decorationContent, decorationEmptyClass, command, items, render, allow, findSuggestionMatch, }: SuggestionOptions<I, TSelected>): Plugin<any>;
/**
* Programmatically exit a suggestion plugin by dispatching a metadata-only
* transaction. This is the safe, recommended API to remove suggestion
* decorations without touching the document or causing mapping errors.
*/
declare function exitSuggestion(view: EditorView, pluginKeyRef?: PluginKey): void;
export { Suggestion, type SuggestionKeyDownProps, type SuggestionMatch, type SuggestionOptions, SuggestionPluginKey, type SuggestionProps, type Trigger, Suggestion as default, findSuggestionMatch };
export { Suggestion, type SuggestionKeyDownProps, type SuggestionMatch, type SuggestionOptions, SuggestionPluginKey, type SuggestionProps, type Trigger, Suggestion as default, exitSuggestion, findSuggestionMatch };

@@ -50,2 +50,3 @@ // src/suggestion.ts

// src/suggestion.ts
var clickHandlerMap = /* @__PURE__ */ new WeakMap();
var SuggestionPluginKey = new PluginKey("suggestion");

@@ -72,5 +73,75 @@ function Suggestion({

const renderer = render == null ? void 0 : render();
const clientRectFor = (view, decorationNode) => {
if (!decorationNode) {
return null;
}
return () => {
const state = pluginKey.getState(editor.state);
const decorationId = state == null ? void 0 : state.decorationId;
const currentDecorationNode = view.dom.querySelector(`[data-decoration-id="${decorationId}"]`);
return (currentDecorationNode == null ? void 0 : currentDecorationNode.getBoundingClientRect()) || null;
};
};
function dispatchExit(view, pluginKeyRef) {
var _a;
try {
const state = pluginKey.getState(view.state);
const decorationNode = (state == null ? void 0 : state.decorationId) ? view.dom.querySelector(`[data-decoration-id="${state.decorationId}"]`) : null;
const exitProps = {
// @ts-ignore editor is available in closure
editor,
range: (state == null ? void 0 : state.range) || { from: 0, to: 0 },
query: (state == null ? void 0 : state.query) || null,
text: (state == null ? void 0 : state.text) || null,
items: [],
command: (commandProps) => {
return command({ editor, range: (state == null ? void 0 : state.range) || { from: 0, to: 0 }, props: commandProps });
},
decorationNode,
clientRect: clientRectFor(view, decorationNode)
};
(_a = renderer == null ? void 0 : renderer.onExit) == null ? void 0 : _a.call(renderer, exitProps);
} catch {
}
const tr = view.state.tr.setMeta(pluginKeyRef, { exit: true });
view.dispatch(tr);
}
const plugin = new Plugin({
key: pluginKey,
view() {
view(editorView) {
const ensureClickHandler = (view) => {
if (clickHandlerMap.has(view)) {
return;
}
const handler = (event) => {
if (!props) {
return;
}
const decorationNode = props.decorationNode;
const target = event.target;
if (!decorationNode) {
return;
}
if (target && decorationNode.contains(target)) {
return;
}
if (target && view.dom.contains(target)) {
return;
}
if (target && target.closest && target.closest(".react-renderer")) {
return;
}
dispatchExit(view, pluginKey);
};
document.addEventListener("mousedown", handler, true);
clickHandlerMap.set(view, handler);
};
const removeClickHandler = (view) => {
const handler = clickHandlerMap.get(view);
if (!handler) {
return;
}
document.removeEventListener("mousedown", handler, true);
clickHandlerMap.delete(view);
};
return {

@@ -107,10 +178,3 @@ update: async (view, prevState) => {

decorationNode,
// virtual node for positioning
// this can be used for building popups without a DOM node
clientRect: decorationNode ? () => {
var _a2;
const { decorationId } = (_a2 = this.key) == null ? void 0 : _a2.getState(editor.state);
const currentDecorationNode = view.dom.querySelector(`[data-decoration-id="${decorationId}"]`);
return (currentDecorationNode == null ? void 0 : currentDecorationNode.getBoundingClientRect()) || null;
} : null
clientRect: clientRectFor(view, decorationNode)
};

@@ -138,5 +202,11 @@ if (handleStart) {

}
if (next.active) {
ensureClickHandler(view);
} else {
removeClickHandler(editorView);
}
},
destroy: () => {
var _a;
removeClickHandler(editorView);
if (!props) {

@@ -171,2 +241,11 @@ return;

const next = { ...prev };
const meta = transaction.getMeta(pluginKey);
if (meta && meta.exit) {
next.active = false;
next.decorationId = null;
next.range = { from: 0, to: 0 };
next.query = null;
next.text = null;
return next;
}
next.composing = composing;

@@ -215,3 +294,3 @@ if (isEditable && (empty || editor.view.composing)) {

handleKeyDown(view, event) {
var _a;
var _a, _b, _c;
const { active, range } = plugin.getState(view.state);

@@ -221,3 +300,29 @@ if (!active) {

}
return ((_a = renderer == null ? void 0 : renderer.onKeyDown) == null ? void 0 : _a.call(renderer, { view, event, range })) || false;
if (event.key === "Escape" || event.key === "Esc") {
const state = plugin.getState(view.state);
const cachedNode = (_a = props == null ? void 0 : props.decorationNode) != null ? _a : null;
const decorationNode = cachedNode != null ? cachedNode : (state == null ? void 0 : state.decorationId) ? view.dom.querySelector(`[data-decoration-id="${state.decorationId}"]`) : null;
const exitProps = {
editor,
range: state.range,
query: state.query,
text: state.text,
items: [],
command: (commandProps) => {
return command({ editor, range: state.range, props: commandProps });
},
decorationNode,
// If we have a cached decoration node, use it for the clientRect
// to avoid another DOM lookup. If not, leave clientRect null and
// let consumer decide if they want to query.
clientRect: decorationNode ? () => {
return decorationNode.getBoundingClientRect() || null;
} : null
};
(_b = renderer == null ? void 0 : renderer.onExit) == null ? void 0 : _b.call(renderer, exitProps);
dispatchExit(view, pluginKey);
return true;
}
const handled = ((_c = renderer == null ? void 0 : renderer.onKeyDown) == null ? void 0 : _c.call(renderer, { view, event, range })) || false;
return handled;
},

@@ -248,2 +353,6 @@ // Setup decorator on the currently active suggestion.

}
function exitSuggestion(view, pluginKeyRef = SuggestionPluginKey) {
const tr = view.state.tr.setMeta(pluginKeyRef, { exit: true });
view.dispatch(tr);
}

@@ -256,4 +365,5 @@ // src/index.ts

index_default as default,
exitSuggestion,
findSuggestionMatch
};
//# sourceMappingURL=index.js.map

10

package.json
{
"name": "@tiptap/suggestion",
"description": "suggestion plugin for tiptap",
"version": "3.2.2",
"version": "3.3.0",
"homepage": "https://tiptap.dev",

@@ -34,8 +34,8 @@ "keywords": [

"devDependencies": {
"@tiptap/core": "^3.2.2",
"@tiptap/pm": "^3.2.2"
"@tiptap/core": "^3.3.0",
"@tiptap/pm": "^3.3.0"
},
"peerDependencies": {
"@tiptap/core": "^3.2.2",
"@tiptap/pm": "^3.2.2"
"@tiptap/core": "^3.3.0",
"@tiptap/pm": "^3.3.0"
},

@@ -42,0 +42,0 @@ "repository": {

@@ -1,2 +0,2 @@

import { Suggestion } from './suggestion.js'
import { exitSuggestion, Suggestion } from './suggestion.js'

@@ -6,2 +6,4 @@ export * from './findSuggestionMatch.js'

export { exitSuggestion }
export default Suggestion

@@ -9,2 +9,7 @@ import type { Editor, Range } from '@tiptap/core'

// Track document click handlers per EditorView instance to avoid accidental
// leaks when multiple editors are created/destroyed. WeakMap ensures handlers
// don't keep views alive.
const clickHandlerMap: WeakMap<EditorView, (event: MouseEvent) => void> = new WeakMap()
export interface SuggestionOptions<I = any, TSelected = any> {

@@ -209,6 +214,105 @@ /**

// Helper to create a clientRect callback for a given decoration node.
// Returns null when no decoration node is present. Uses the pluginKey's
// state to resolve the current decoration node on demand, avoiding a
// duplicated implementation in multiple places.
const clientRectFor = (view: EditorView, decorationNode: Element | null) => {
if (!decorationNode) {
return null
}
return () => {
const state = pluginKey.getState(editor.state)
const decorationId = state?.decorationId
const currentDecorationNode = view.dom.querySelector(`[data-decoration-id="${decorationId}"]`)
return currentDecorationNode?.getBoundingClientRect() || null
}
}
// small helper used internally by the view to dispatch an exit
function dispatchExit(view: EditorView, pluginKeyRef: PluginKey) {
try {
// Try to call renderer.onExit so consumer renderers (for example the
// demos' ReactRenderer) can clean up and unmount immediately. This
// covers paths where we only dispatch a metadata transaction (like
// click-outside) and ensures we don't leak DOM nodes / React roots.
const state = pluginKey.getState(view.state)
const decorationNode = state?.decorationId
? view.dom.querySelector(`[data-decoration-id="${state.decorationId}"]`)
: null
const exitProps: SuggestionProps = {
// @ts-ignore editor is available in closure
editor,
range: state?.range || { from: 0, to: 0 },
query: state?.query || null,
text: state?.text || null,
items: [],
command: commandProps => {
return command({ editor, range: state?.range || { from: 0, to: 0 }, props: commandProps as any })
},
decorationNode,
clientRect: clientRectFor(view, decorationNode),
}
renderer?.onExit?.(exitProps)
} catch {
// ignore errors from consumer renderers
}
const tr = view.state.tr.setMeta(pluginKeyRef, { exit: true })
// Dispatch a metadata-only transaction to signal the plugin to exit
view.dispatch(tr)
}
const plugin: Plugin<any> = new Plugin({
key: pluginKey,
view() {
view(editorView: EditorView) {
const ensureClickHandler = (view: EditorView) => {
if (clickHandlerMap.has(view)) {
return
}
const handler = (event: MouseEvent) => {
if (!props) {
return
}
const decorationNode = props.decorationNode
const target = event.target as Element | null
if (!decorationNode) {
return
}
if (target && decorationNode.contains(target)) {
return
}
if (target && view.dom.contains(target)) {
return
}
if (target && target.closest && target.closest('.react-renderer')) {
return
}
dispatchExit(view, pluginKey)
}
document.addEventListener('mousedown', handler, true)
clickHandlerMap.set(view, handler)
}
const removeClickHandler = (view: EditorView) => {
const handler = clickHandlerMap.get(view)
if (!handler) {
return
}
document.removeEventListener('mousedown', handler, true)
clickHandlerMap.delete(view)
}
return {

@@ -251,13 +355,3 @@ update: async (view, prevState) => {

decorationNode,
// virtual node for positioning
// this can be used for building popups without a DOM node
clientRect: decorationNode
? () => {
// because of `items` can be asynchrounous we’ll search for the current decoration node
const { decorationId } = this.key?.getState(editor.state) // eslint-disable-line
const currentDecorationNode = view.dom.querySelector(`[data-decoration-id="${decorationId}"]`)
return currentDecorationNode?.getBoundingClientRect() || null
}
: null,
clientRect: clientRectFor(view, decorationNode),
}

@@ -291,5 +385,14 @@

}
// Install / remove click handler depending on suggestion active state
if (next.active) {
ensureClickHandler(view)
} else {
removeClickHandler(editorView)
}
},
destroy: () => {
removeClickHandler(editorView)
if (!props) {

@@ -336,2 +439,17 @@ return

// If a transaction carries the exit meta for this plugin, immediately
// deactivate the suggestion. This allows metadata-only transactions
// (dispatched by escape or programmatic exit) to deterministically
// clear decorations without changing the document.
const meta = transaction.getMeta(pluginKey)
if (meta && meta.exit) {
next.active = false
next.decorationId = null
next.range = { from: 0, to: 0 }
next.query = null
next.text = null
return next
}
next.composing = composing

@@ -402,3 +520,43 @@

return renderer?.onKeyDown?.({ view, event, range }) || false
// If Escape is pressed, call onExit and dispatch a metadata-only
// transaction to unset the suggestion state. This provides a safe
// and deterministic way to exit the suggestion without altering the
// document (avoids transaction mapping/mismatch issues).
if (event.key === 'Escape' || event.key === 'Esc') {
const state = plugin.getState(view.state)
const cachedNode = props?.decorationNode ?? null
const decorationNode =
cachedNode ??
(state?.decorationId ? view.dom.querySelector(`[data-decoration-id="${state.decorationId}"]`) : null)
const exitProps: SuggestionProps = {
editor,
range: state.range,
query: state.query,
text: state.text,
items: [],
command: commandProps => {
return command({ editor, range: state.range, props: commandProps as any })
},
decorationNode,
// If we have a cached decoration node, use it for the clientRect
// to avoid another DOM lookup. If not, leave clientRect null and
// let consumer decide if they want to query.
clientRect: decorationNode
? () => {
return decorationNode.getBoundingClientRect() || null
}
: null,
}
renderer?.onExit?.(exitProps)
// dispatch metadata-only transaction to unset the plugin state
dispatchExit(view, pluginKey)
return true
}
const handled = renderer?.onKeyDown?.({ view, event, range }) || false
return handled
},

@@ -435,1 +593,11 @@

}
/**
* Programmatically exit a suggestion plugin by dispatching a metadata-only
* transaction. This is the safe, recommended API to remove suggestion
* decorations without touching the document or causing mapping errors.
*/
export function exitSuggestion(view: EditorView, pluginKeyRef: PluginKey = SuggestionPluginKey) {
const tr = view.state.tr.setMeta(pluginKeyRef, { exit: true })
view.dispatch(tr)
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

About

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.

  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc

U.S. Patent No. 12,346,443 & 12,314,394. Other pending.