@tiptap/suggestion 3.26.1 → 3.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +619 -270
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +193 -2
- package/dist/index.d.ts +193 -2
- package/dist/index.js +624 -270
- package/dist/index.js.map +1 -1
- package/package.json +10 -5
- package/src/__tests__/suggestion.test.ts +837 -0
- package/src/helpers.ts +129 -0
- package/src/plugin/async.ts +89 -0
- package/src/plugin/floating-ui.ts +204 -0
- package/src/plugin/props.ts +94 -0
- package/src/plugin/state.ts +182 -0
- package/src/plugin/view.ts +236 -0
- package/src/suggestion.ts +97 -606
- package/src/types.ts +439 -0
package/src/suggestion.ts
CHANGED
|
@@ -1,243 +1,41 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Range } from '@tiptap/core'
|
|
2
2
|
import type { EditorState, Transaction } from '@tiptap/pm/state'
|
|
3
3
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
|
4
4
|
import type { EditorView } from '@tiptap/pm/view'
|
|
5
|
-
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
|
6
5
|
|
|
7
6
|
import type { SuggestionMatch } from './findSuggestionMatch.js'
|
|
8
7
|
import { findSuggestionMatch as defaultFindSuggestionMatch } from './findSuggestionMatch.js'
|
|
8
|
+
import {
|
|
9
|
+
clientRectFor as clientRectForHelper,
|
|
10
|
+
dispatchExit as dispatchExitHelper,
|
|
11
|
+
shouldKeepDismissed as shouldKeepDismissedHelper,
|
|
12
|
+
} from './helpers.js'
|
|
13
|
+
import { createSuggestionProps } from './plugin/props.js'
|
|
14
|
+
import { createSuggestionState } from './plugin/state.js'
|
|
15
|
+
import { createSuggestionView } from './plugin/view.js'
|
|
16
|
+
import type { SuggestionOptions } from './types.js'
|
|
17
|
+
|
|
18
|
+
export type {
|
|
19
|
+
SuggestionFloatingUiConfig,
|
|
20
|
+
SuggestionFloatingUiOptions,
|
|
21
|
+
SuggestionKeyDownProps,
|
|
22
|
+
SuggestionMount,
|
|
23
|
+
SuggestionMountOptions,
|
|
24
|
+
SuggestionOptions,
|
|
25
|
+
SuggestionPlacement,
|
|
26
|
+
SuggestionPositionData,
|
|
27
|
+
SuggestionProps,
|
|
28
|
+
} from './types.js'
|
|
9
29
|
|
|
10
|
-
|
|
11
|
-
* Returns true if the transaction inserted any whitespace or newline character.
|
|
12
|
-
* Used to determine when a dismissed suggestion should become active again.
|
|
13
|
-
*/
|
|
14
|
-
function hasInsertedWhitespace(transaction: Transaction): boolean {
|
|
15
|
-
if (!transaction.docChanged) {
|
|
16
|
-
return false
|
|
17
|
-
}
|
|
18
|
-
return transaction.steps.some(step => {
|
|
19
|
-
const slice = (step as any).slice
|
|
20
|
-
if (!slice?.content) {
|
|
21
|
-
return false
|
|
22
|
-
}
|
|
23
|
-
// textBetween with '\n' as block separator catches both inline spaces and newlines
|
|
24
|
-
const inserted = slice.content.textBetween(0, slice.content.size, '\n')
|
|
25
|
-
return /\s/.test(inserted)
|
|
26
|
-
})
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface SuggestionOptions<I = any, TSelected = any> {
|
|
30
|
-
/**
|
|
31
|
-
* The plugin key for the suggestion plugin.
|
|
32
|
-
* @default 'suggestion'
|
|
33
|
-
* @example 'mention'
|
|
34
|
-
*/
|
|
35
|
-
pluginKey?: PluginKey
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* A function that returns a boolean to indicate if the suggestion should be active.
|
|
39
|
-
* This is useful to prevent suggestions from opening for remote users in collaborative environments.
|
|
40
|
-
* @param props The props object.
|
|
41
|
-
* @param props.editor The editor instance.
|
|
42
|
-
* @param props.range The range of the suggestion.
|
|
43
|
-
* @param props.query The current suggestion query.
|
|
44
|
-
* @param props.text The current suggestion text.
|
|
45
|
-
* @param props.transaction The current transaction.
|
|
46
|
-
* @returns {boolean}
|
|
47
|
-
* @example ({ transaction }) => isChangeOrigin(transaction)
|
|
48
|
-
*/
|
|
49
|
-
shouldShow?: (props: {
|
|
50
|
-
editor: Editor
|
|
51
|
-
range: Range
|
|
52
|
-
query: string
|
|
53
|
-
text: string
|
|
54
|
-
transaction: Transaction
|
|
55
|
-
}) => boolean
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Controls when a dismissed suggestion becomes active again.
|
|
59
|
-
* Return `true` to clear the dismissed context for the current transaction.
|
|
60
|
-
*/
|
|
61
|
-
shouldResetDismissed?: (props: {
|
|
62
|
-
editor: Editor
|
|
63
|
-
state: EditorState
|
|
64
|
-
range: Range
|
|
65
|
-
match: Exclude<SuggestionMatch, null>
|
|
66
|
-
transaction: Transaction
|
|
67
|
-
allowSpaces: boolean
|
|
68
|
-
}) => boolean
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* The editor instance.
|
|
72
|
-
* @default null
|
|
73
|
-
*/
|
|
74
|
-
editor: Editor
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* The character that triggers the suggestion.
|
|
78
|
-
* @default '@'
|
|
79
|
-
* @example '#'
|
|
80
|
-
*/
|
|
81
|
-
char?: string
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Allow spaces in the suggestion query. Not compatible with `allowToIncludeChar`. Will be disabled if `allowToIncludeChar` is set to `true`.
|
|
85
|
-
* @default false
|
|
86
|
-
* @example true
|
|
87
|
-
*/
|
|
88
|
-
allowSpaces?: boolean
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Allow the character to be included in the suggestion query. Not compatible with `allowSpaces`.
|
|
92
|
-
* @default false
|
|
93
|
-
*/
|
|
94
|
-
allowToIncludeChar?: boolean
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Allow prefixes in the suggestion query.
|
|
98
|
-
* @default [' ']
|
|
99
|
-
* @example [' ', '@']
|
|
100
|
-
*/
|
|
101
|
-
allowedPrefixes?: string[] | null
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Only match suggestions at the start of the line.
|
|
105
|
-
* @default false
|
|
106
|
-
* @example true
|
|
107
|
-
*/
|
|
108
|
-
startOfLine?: boolean
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* The tag name of the decoration node.
|
|
112
|
-
* @default 'span'
|
|
113
|
-
* @example 'div'
|
|
114
|
-
*/
|
|
115
|
-
decorationTag?: string
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* The class name of the decoration node.
|
|
119
|
-
* @default 'suggestion'
|
|
120
|
-
* @example 'mention'
|
|
121
|
-
*/
|
|
122
|
-
decorationClass?: string
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Creates a decoration with the provided content.
|
|
126
|
-
* @param decorationContent - The content to display in the decoration
|
|
127
|
-
* @default "" - Creates an empty decoration if no content provided
|
|
128
|
-
*/
|
|
129
|
-
decorationContent?: string
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* The class name of the decoration node when it is empty.
|
|
133
|
-
* @default 'is-empty'
|
|
134
|
-
* @example 'is-empty'
|
|
135
|
-
*/
|
|
136
|
-
decorationEmptyClass?: string
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* A function that is called when a suggestion is selected.
|
|
140
|
-
* @param props The props object.
|
|
141
|
-
* @param props.editor The editor instance.
|
|
142
|
-
* @param props.range The range of the suggestion.
|
|
143
|
-
* @param props.props The props of the selected suggestion.
|
|
144
|
-
* @returns void
|
|
145
|
-
* @example ({ editor, range, props }) => { props.command(props.props) }
|
|
146
|
-
*/
|
|
147
|
-
command?: (props: { editor: Editor; range: Range; props: TSelected }) => void
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* A function that returns the suggestion items in form of an array.
|
|
151
|
-
* @param props The props object.
|
|
152
|
-
* @param props.editor The editor instance.
|
|
153
|
-
* @param props.query The current suggestion query.
|
|
154
|
-
* @returns An array of suggestion items.
|
|
155
|
-
* @example ({ editor, query }) => [{ id: 1, label: 'John Doe' }]
|
|
156
|
-
*/
|
|
157
|
-
items?: (props: { query: string; editor: Editor }) => I[] | Promise<I[]>
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* The render function for the suggestion.
|
|
161
|
-
* @returns An object with render functions.
|
|
162
|
-
*/
|
|
163
|
-
render?: () => {
|
|
164
|
-
onBeforeStart?: (props: SuggestionProps<I, TSelected>) => void
|
|
165
|
-
onStart?: (props: SuggestionProps<I, TSelected>) => void
|
|
166
|
-
onBeforeUpdate?: (props: SuggestionProps<I, TSelected>) => void
|
|
167
|
-
onUpdate?: (props: SuggestionProps<I, TSelected>) => void
|
|
168
|
-
onExit?: (props: SuggestionProps<I, TSelected>) => void
|
|
169
|
-
onKeyDown?: (props: SuggestionKeyDownProps) => boolean
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* A function that returns a boolean to indicate if the suggestion should be active.
|
|
174
|
-
* @param props The props object.
|
|
175
|
-
* @returns {boolean}
|
|
176
|
-
*/
|
|
177
|
-
allow?: (props: {
|
|
178
|
-
editor: Editor
|
|
179
|
-
state: EditorState
|
|
180
|
-
range: Range
|
|
181
|
-
isActive?: boolean
|
|
182
|
-
}) => boolean
|
|
183
|
-
findSuggestionMatch?: typeof defaultFindSuggestionMatch
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export interface SuggestionProps<I = any, TSelected = any> {
|
|
187
|
-
/**
|
|
188
|
-
* The editor instance.
|
|
189
|
-
*/
|
|
190
|
-
editor: Editor
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* The range of the suggestion.
|
|
194
|
-
*/
|
|
195
|
-
range: Range
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* The current suggestion query.
|
|
199
|
-
*/
|
|
200
|
-
query: string
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* The current suggestion text.
|
|
204
|
-
*/
|
|
205
|
-
text: string
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* The suggestion items array.
|
|
209
|
-
*/
|
|
210
|
-
items: I[]
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* A function that is called when a suggestion is selected.
|
|
214
|
-
* @param props The props object.
|
|
215
|
-
* @returns void
|
|
216
|
-
*/
|
|
217
|
-
command: (props: TSelected) => void
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* The decoration node HTML element
|
|
221
|
-
* @default null
|
|
222
|
-
*/
|
|
223
|
-
decorationNode: Element | null
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* The function that returns the client rect
|
|
227
|
-
* @default null
|
|
228
|
-
* @example () => new DOMRect(0, 0, 0, 0)
|
|
229
|
-
*/
|
|
230
|
-
clientRect?: (() => DOMRect | null) | null
|
|
231
|
-
}
|
|
30
|
+
export const SuggestionPluginKey = new PluginKey('suggestion')
|
|
232
31
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
32
|
+
type ShouldKeepDismissedProps = {
|
|
33
|
+
match: Exclude<SuggestionMatch, null>
|
|
34
|
+
dismissedRange: Range
|
|
35
|
+
state: EditorState
|
|
36
|
+
transaction: Transaction
|
|
237
37
|
}
|
|
238
38
|
|
|
239
|
-
export const SuggestionPluginKey = new PluginKey('suggestion')
|
|
240
|
-
|
|
241
39
|
/**
|
|
242
40
|
* This utility allows you to create suggestions.
|
|
243
41
|
* @see https://tiptap.dev/api/utilities/suggestion
|
|
@@ -256,397 +54,90 @@ export function Suggestion<I = any, TSelected = any>({
|
|
|
256
54
|
decorationEmptyClass = 'is-empty',
|
|
257
55
|
command = () => null,
|
|
258
56
|
items = () => [],
|
|
57
|
+
minQueryLength = 0,
|
|
58
|
+
debounce = 0,
|
|
59
|
+
initialItems,
|
|
60
|
+
placement = 'bottom-start',
|
|
61
|
+
offset: offsetOption = {},
|
|
62
|
+
container,
|
|
63
|
+
flip = true,
|
|
64
|
+
floatingUi,
|
|
65
|
+
dismissOnOutsideClick = true,
|
|
259
66
|
render = () => ({}),
|
|
260
67
|
allow = () => true,
|
|
261
68
|
findSuggestionMatch = defaultFindSuggestionMatch,
|
|
262
69
|
shouldShow,
|
|
263
70
|
shouldResetDismissed,
|
|
264
71
|
}: SuggestionOptions<I, TSelected>) {
|
|
265
|
-
let props: SuggestionProps<I, TSelected> | undefined
|
|
266
72
|
const renderer = render?.()
|
|
267
73
|
const effectiveAllowSpaces = allowSpaces && !allowToIncludeChar
|
|
268
74
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Helper to create a clientRect callback for a given decoration node.
|
|
284
|
-
// Returns null when no decoration node is present. Uses the pluginKey's
|
|
285
|
-
// state to resolve the current decoration node on demand, avoiding a
|
|
286
|
-
// duplicated implementation in multiple places.
|
|
287
|
-
const clientRectFor = (view: EditorView, decorationNode: Element | null) => {
|
|
288
|
-
if (!decorationNode) {
|
|
289
|
-
return getAnchorClientRect
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return () => {
|
|
293
|
-
const state = pluginKey.getState(editor.state)
|
|
294
|
-
const decorationId = state?.decorationId
|
|
295
|
-
const currentDecorationNode = view.dom.querySelector(`[data-decoration-id="${decorationId}"]`)
|
|
296
|
-
|
|
297
|
-
return currentDecorationNode?.getBoundingClientRect() || null
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const shouldKeepDismissed = ({
|
|
302
|
-
match,
|
|
303
|
-
dismissedRange,
|
|
304
|
-
state,
|
|
305
|
-
transaction,
|
|
306
|
-
}: {
|
|
307
|
-
match: Exclude<SuggestionMatch, null>
|
|
308
|
-
dismissedRange: Range
|
|
309
|
-
state: EditorState
|
|
310
|
-
transaction: Transaction
|
|
311
|
-
}) => {
|
|
312
|
-
if (
|
|
313
|
-
shouldResetDismissed?.({
|
|
314
|
-
editor,
|
|
315
|
-
state,
|
|
316
|
-
range: dismissedRange,
|
|
317
|
-
match,
|
|
318
|
-
transaction,
|
|
319
|
-
allowSpaces: effectiveAllowSpaces,
|
|
320
|
-
})
|
|
321
|
-
) {
|
|
322
|
-
return false
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (effectiveAllowSpaces) {
|
|
326
|
-
return match.range.from === dismissedRange.from
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return match.range.from === dismissedRange.from && !hasInsertedWhitespace(transaction)
|
|
75
|
+
const clientRectFor = (view: EditorView, decorationNode: Element | null) =>
|
|
76
|
+
clientRectForHelper(editor, view, decorationNode, pluginKey)
|
|
77
|
+
|
|
78
|
+
// helper to check if the dismissed suggestion should stay dismissed, with access to editor and options
|
|
79
|
+
function shouldKeepDismissed(props: ShouldKeepDismissedProps) {
|
|
80
|
+
return shouldKeepDismissedHelper({
|
|
81
|
+
...props,
|
|
82
|
+
editor,
|
|
83
|
+
shouldResetDismissed,
|
|
84
|
+
effectiveAllowSpaces,
|
|
85
|
+
})
|
|
330
86
|
}
|
|
331
87
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
? view.dom.querySelector(`[data-decoration-id="${state.decorationId}"]`)
|
|
338
|
-
: null
|
|
88
|
+
const dispatchExit = (view: EditorView) =>
|
|
89
|
+
dispatchExitHelper({
|
|
90
|
+
view,
|
|
91
|
+
pluginKeyRef: pluginKey,
|
|
92
|
+
})
|
|
339
93
|
|
|
340
|
-
|
|
341
|
-
// @ts-ignore editor is available in closure
|
|
342
|
-
editor,
|
|
343
|
-
range: state?.range || { from: 0, to: 0 },
|
|
344
|
-
query: state?.query || null,
|
|
345
|
-
text: state?.text || null,
|
|
346
|
-
items: [],
|
|
347
|
-
command: commandProps => {
|
|
348
|
-
return command({
|
|
349
|
-
editor,
|
|
350
|
-
range: state?.range || { from: 0, to: 0 },
|
|
351
|
-
props: commandProps as any,
|
|
352
|
-
})
|
|
353
|
-
},
|
|
354
|
-
decorationNode,
|
|
355
|
-
clientRect: clientRectFor(view, decorationNode),
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
renderer?.onExit?.(exitProps)
|
|
359
|
-
} catch {
|
|
360
|
-
// ignore errors from consumer renderers
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const tr = view.state.tr.setMeta(pluginKeyRef, { exit: true })
|
|
364
|
-
// Dispatch a metadata-only transaction to signal the plugin to exit
|
|
365
|
-
view.dispatch(tr)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const plugin: Plugin<any> = new Plugin({
|
|
94
|
+
return new Plugin({
|
|
369
95
|
key: pluginKey,
|
|
370
96
|
|
|
371
|
-
view()
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (handleStart) {
|
|
415
|
-
renderer?.onBeforeStart?.(props)
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (handleChange) {
|
|
419
|
-
renderer?.onBeforeUpdate?.(props)
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if (handleChange || handleStart) {
|
|
423
|
-
props.items = await items({
|
|
424
|
-
editor,
|
|
425
|
-
query: state.query,
|
|
426
|
-
})
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (handleExit) {
|
|
430
|
-
renderer?.onExit?.(props)
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (handleChange) {
|
|
434
|
-
renderer?.onUpdate?.(props)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (handleStart) {
|
|
438
|
-
renderer?.onStart?.(props)
|
|
439
|
-
}
|
|
440
|
-
},
|
|
441
|
-
|
|
442
|
-
destroy: () => {
|
|
443
|
-
if (!props) {
|
|
444
|
-
return
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
renderer?.onExit?.(props)
|
|
448
|
-
},
|
|
449
|
-
}
|
|
450
|
-
},
|
|
451
|
-
|
|
452
|
-
state: {
|
|
453
|
-
// Initialize the plugin's internal state.
|
|
454
|
-
init() {
|
|
455
|
-
const state: {
|
|
456
|
-
active: boolean
|
|
457
|
-
range: Range
|
|
458
|
-
query: null | string
|
|
459
|
-
text: null | string
|
|
460
|
-
composing: boolean
|
|
461
|
-
decorationId?: string | null
|
|
462
|
-
dismissedRange: Range | null
|
|
463
|
-
} = {
|
|
464
|
-
active: false,
|
|
465
|
-
range: {
|
|
466
|
-
from: 0,
|
|
467
|
-
to: 0,
|
|
468
|
-
},
|
|
469
|
-
query: null,
|
|
470
|
-
text: null,
|
|
471
|
-
composing: false,
|
|
472
|
-
dismissedRange: null,
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return state
|
|
476
|
-
},
|
|
477
|
-
|
|
478
|
-
// Apply changes to the plugin state from a view transaction.
|
|
479
|
-
apply(transaction, prev, _oldState, state) {
|
|
480
|
-
const { isEditable } = editor
|
|
481
|
-
const { composing } = editor.view
|
|
482
|
-
const { selection } = transaction
|
|
483
|
-
const { empty, from } = selection
|
|
484
|
-
const next = { ...prev }
|
|
485
|
-
|
|
486
|
-
// If a transaction carries the exit meta for this plugin, immediately
|
|
487
|
-
// deactivate the suggestion. This allows metadata-only transactions
|
|
488
|
-
// (dispatched by escape or programmatic exit) to deterministically
|
|
489
|
-
// clear decorations without changing the document.
|
|
490
|
-
const meta = transaction.getMeta(pluginKey)
|
|
491
|
-
if (meta && meta.exit) {
|
|
492
|
-
next.active = false
|
|
493
|
-
next.decorationId = null
|
|
494
|
-
next.range = { from: 0, to: 0 }
|
|
495
|
-
next.query = null
|
|
496
|
-
next.text = null
|
|
497
|
-
next.dismissedRange = prev.active ? { ...prev.range } : prev.dismissedRange
|
|
498
|
-
|
|
499
|
-
return next
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
next.composing = composing
|
|
503
|
-
|
|
504
|
-
if (transaction.docChanged && next.dismissedRange !== null) {
|
|
505
|
-
next.dismissedRange = {
|
|
506
|
-
from: transaction.mapping.map(next.dismissedRange.from),
|
|
507
|
-
to: transaction.mapping.map(next.dismissedRange.to),
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// We can only be suggesting if the view is editable, and:
|
|
512
|
-
// * there is no selection, or
|
|
513
|
-
// * a composition is active (see: https://github.com/ueberdosis/tiptap/issues/1449)
|
|
514
|
-
if (isEditable && (empty || editor.view.composing)) {
|
|
515
|
-
// Reset active state if we just left the previous suggestion range
|
|
516
|
-
if ((from < prev.range.from || from > prev.range.to) && !composing && !prev.composing) {
|
|
517
|
-
next.active = false
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Try to match against where our cursor currently is
|
|
521
|
-
const match = findSuggestionMatch({
|
|
522
|
-
char,
|
|
523
|
-
allowSpaces,
|
|
524
|
-
allowToIncludeChar,
|
|
525
|
-
allowedPrefixes,
|
|
526
|
-
startOfLine,
|
|
527
|
-
$position: selection.$from,
|
|
528
|
-
})
|
|
529
|
-
const decorationId = `id_${Math.floor(Math.random() * 0xffffffff)}`
|
|
530
|
-
|
|
531
|
-
// If we found a match, update the current state to show it
|
|
532
|
-
if (
|
|
533
|
-
match &&
|
|
534
|
-
allow({
|
|
535
|
-
editor,
|
|
536
|
-
state,
|
|
537
|
-
range: match.range,
|
|
538
|
-
isActive: prev.active,
|
|
539
|
-
}) &&
|
|
540
|
-
(!shouldShow ||
|
|
541
|
-
shouldShow({
|
|
542
|
-
editor,
|
|
543
|
-
range: match.range,
|
|
544
|
-
query: match.query,
|
|
545
|
-
text: match.text,
|
|
546
|
-
transaction,
|
|
547
|
-
}))
|
|
548
|
-
) {
|
|
549
|
-
if (
|
|
550
|
-
next.dismissedRange !== null &&
|
|
551
|
-
!shouldKeepDismissed({
|
|
552
|
-
match,
|
|
553
|
-
dismissedRange: next.dismissedRange,
|
|
554
|
-
state,
|
|
555
|
-
transaction,
|
|
556
|
-
})
|
|
557
|
-
) {
|
|
558
|
-
next.dismissedRange = null
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (next.dismissedRange === null) {
|
|
562
|
-
next.active = true
|
|
563
|
-
next.decorationId = prev.decorationId ? prev.decorationId : decorationId
|
|
564
|
-
next.range = match.range
|
|
565
|
-
next.query = match.query
|
|
566
|
-
next.text = match.text
|
|
567
|
-
} else {
|
|
568
|
-
next.active = false
|
|
569
|
-
}
|
|
570
|
-
} else {
|
|
571
|
-
if (!match) {
|
|
572
|
-
next.dismissedRange = null
|
|
573
|
-
}
|
|
574
|
-
next.active = false
|
|
575
|
-
}
|
|
576
|
-
} else {
|
|
577
|
-
next.active = false
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Make sure to empty the range if suggestion is inactive
|
|
581
|
-
if (!next.active) {
|
|
582
|
-
next.decorationId = null
|
|
583
|
-
next.range = { from: 0, to: 0 }
|
|
584
|
-
next.query = null
|
|
585
|
-
next.text = null
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
return next
|
|
589
|
-
},
|
|
590
|
-
},
|
|
591
|
-
|
|
592
|
-
props: {
|
|
593
|
-
// Call the keydown hook if suggestion is active.
|
|
594
|
-
handleKeyDown(view, event) {
|
|
595
|
-
const { active, range } = plugin.getState(view.state)
|
|
596
|
-
|
|
597
|
-
if (!active) {
|
|
598
|
-
return false
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// If Escape is pressed, call onExit and dispatch a metadata-only
|
|
602
|
-
// transaction to unset the suggestion state. This provides a safe
|
|
603
|
-
// and deterministic way to exit the suggestion without altering the
|
|
604
|
-
// document (avoids transaction mapping/mismatch issues).
|
|
605
|
-
if (event.key === 'Escape' || event.key === 'Esc') {
|
|
606
|
-
const state = plugin.getState(view.state)
|
|
607
|
-
|
|
608
|
-
// Allow the consumer to react to Escape, but always clear the
|
|
609
|
-
// suggestion state afterward so the decoration is removed too.
|
|
610
|
-
renderer?.onKeyDown?.({ view, event, range: state.range })
|
|
611
|
-
|
|
612
|
-
// dispatch metadata-only transaction to unset the plugin state
|
|
613
|
-
dispatchExit(view, pluginKey)
|
|
614
|
-
|
|
615
|
-
return true
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const handled = renderer?.onKeyDown?.({ view, event, range }) || false
|
|
619
|
-
return handled
|
|
620
|
-
},
|
|
621
|
-
|
|
622
|
-
// Setup decorator on the currently active suggestion.
|
|
623
|
-
decorations(state) {
|
|
624
|
-
const { active, range, decorationId, query } = plugin.getState(state)
|
|
625
|
-
|
|
626
|
-
if (!active) {
|
|
627
|
-
return null
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const isEmpty = !query?.length
|
|
631
|
-
const classNames = [decorationClass]
|
|
632
|
-
|
|
633
|
-
if (isEmpty) {
|
|
634
|
-
classNames.push(decorationEmptyClass)
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return DecorationSet.create(state.doc, [
|
|
638
|
-
Decoration.inline(range.from, range.to, {
|
|
639
|
-
nodeName: decorationTag,
|
|
640
|
-
class: classNames.join(' '),
|
|
641
|
-
'data-decoration-id': decorationId,
|
|
642
|
-
'data-decoration-content': decorationContent,
|
|
643
|
-
}),
|
|
644
|
-
])
|
|
645
|
-
},
|
|
646
|
-
},
|
|
97
|
+
view: () =>
|
|
98
|
+
createSuggestionView({
|
|
99
|
+
editor,
|
|
100
|
+
pluginKey,
|
|
101
|
+
items,
|
|
102
|
+
renderer,
|
|
103
|
+
minQueryLength,
|
|
104
|
+
debounce,
|
|
105
|
+
initialItems,
|
|
106
|
+
placement,
|
|
107
|
+
offset: offsetOption,
|
|
108
|
+
container,
|
|
109
|
+
flip,
|
|
110
|
+
floatingUi,
|
|
111
|
+
dismissOnOutsideClick,
|
|
112
|
+
command,
|
|
113
|
+
clientRectFor,
|
|
114
|
+
dispatchExit,
|
|
115
|
+
}),
|
|
116
|
+
|
|
117
|
+
state: createSuggestionState({
|
|
118
|
+
editor,
|
|
119
|
+
char,
|
|
120
|
+
effectiveAllowSpaces,
|
|
121
|
+
allowToIncludeChar,
|
|
122
|
+
allowedPrefixes,
|
|
123
|
+
startOfLine,
|
|
124
|
+
findSuggestionMatch,
|
|
125
|
+
allow,
|
|
126
|
+
shouldShow,
|
|
127
|
+
shouldKeepDismissed,
|
|
128
|
+
pluginKey,
|
|
129
|
+
}),
|
|
130
|
+
|
|
131
|
+
props: createSuggestionProps({
|
|
132
|
+
pluginKey,
|
|
133
|
+
decorationTag,
|
|
134
|
+
decorationClass,
|
|
135
|
+
decorationContent,
|
|
136
|
+
decorationEmptyClass,
|
|
137
|
+
renderer,
|
|
138
|
+
dispatchExit,
|
|
139
|
+
}),
|
|
647
140
|
})
|
|
648
|
-
|
|
649
|
-
return plugin
|
|
650
141
|
}
|
|
651
142
|
|
|
652
143
|
/**
|