@tiptap/core 2.5.0-beta.0 → 2.5.0-beta.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 +92 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +92 -13
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +92 -13
- package/dist/index.umd.js.map +1 -1
- package/dist/packages/core/src/Editor.d.ts +4 -2
- package/dist/packages/core/src/commands/insertContentAt.d.ts +10 -0
- package/dist/packages/core/src/commands/setContent.d.ts +10 -1
- package/dist/packages/core/src/helpers/createDocument.d.ts +3 -1
- package/dist/packages/core/src/helpers/createNodeFromContent.d.ts +1 -0
- package/dist/packages/core/src/types.d.ts +21 -0
- package/package.json +2 -2
- package/src/Editor.ts +45 -4
- package/src/commands/insertContentAt.ts +26 -6
- package/src/commands/setContent.ts +21 -3
- package/src/helpers/createDocument.ts +6 -1
- package/src/helpers/createNodeFromContent.ts +42 -2
- package/src/types.ts +21 -0
|
@@ -6,8 +6,10 @@ import { ExtensionManager } from './ExtensionManager.js';
|
|
|
6
6
|
import { NodePos } from './NodePos.js';
|
|
7
7
|
import { CanCommands, ChainedCommands, EditorEvents, EditorOptions, JSONContent, SingleCommands, TextSerializer } from './types.js';
|
|
8
8
|
export * as extensions from './extensions/index.js';
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
declare global {
|
|
10
|
+
interface HTMLElement {
|
|
11
|
+
editor?: Editor;
|
|
12
|
+
}
|
|
11
13
|
}
|
|
12
14
|
export declare class Editor extends EventEmitter<EditorEvents> {
|
|
13
15
|
private commandManager;
|
|
@@ -28,8 +28,18 @@ declare module '@tiptap/core' {
|
|
|
28
28
|
* Whether to update the selection after inserting the content.
|
|
29
29
|
*/
|
|
30
30
|
updateSelection?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Whether to apply input rules after inserting the content.
|
|
33
|
+
*/
|
|
31
34
|
applyInputRules?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Whether to apply paste rules after inserting the content.
|
|
37
|
+
*/
|
|
32
38
|
applyPasteRules?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Whether to throw an error if the content is invalid.
|
|
41
|
+
*/
|
|
42
|
+
errorOnInvalidContent?: boolean;
|
|
33
43
|
}) => ReturnType;
|
|
34
44
|
};
|
|
35
45
|
}
|
|
@@ -24,7 +24,16 @@ declare module '@tiptap/core' {
|
|
|
24
24
|
* Options for parsing the content.
|
|
25
25
|
* @default {}
|
|
26
26
|
*/
|
|
27
|
-
parseOptions?: ParseOptions
|
|
27
|
+
parseOptions?: ParseOptions,
|
|
28
|
+
/**
|
|
29
|
+
* Options for `setContent`.
|
|
30
|
+
*/
|
|
31
|
+
options?: {
|
|
32
|
+
/**
|
|
33
|
+
* Whether to throw an error if the content is invalid.
|
|
34
|
+
*/
|
|
35
|
+
errorOnInvalidContent?: boolean;
|
|
36
|
+
}) => ReturnType;
|
|
28
37
|
};
|
|
29
38
|
}
|
|
30
39
|
}
|
|
@@ -7,4 +7,6 @@ import { Content } from '../types.js';
|
|
|
7
7
|
* @param parseOptions Options for the parser
|
|
8
8
|
* @returns The created Prosemirror document node
|
|
9
9
|
*/
|
|
10
|
-
export declare function createDocument(content: Content, schema: Schema, parseOptions?: ParseOptions
|
|
10
|
+
export declare function createDocument(content: Content, schema: Schema, parseOptions?: ParseOptions, options?: {
|
|
11
|
+
errorOnInvalidContent?: boolean;
|
|
12
|
+
}): ProseMirrorNode;
|
|
@@ -3,6 +3,7 @@ import { Content } from '../types.js';
|
|
|
3
3
|
export declare type CreateNodeFromContentOptions = {
|
|
4
4
|
slice?: boolean;
|
|
5
5
|
parseOptions?: ParseOptions;
|
|
6
|
+
errorOnInvalidContent?: boolean;
|
|
6
7
|
};
|
|
7
8
|
/**
|
|
8
9
|
* Takes a JSON or HTML content and creates a Prosemirror node or fragment from it.
|
|
@@ -23,6 +23,15 @@ export interface EditorEvents {
|
|
|
23
23
|
create: {
|
|
24
24
|
editor: Editor;
|
|
25
25
|
};
|
|
26
|
+
contentError: {
|
|
27
|
+
editor: Editor;
|
|
28
|
+
error: Error;
|
|
29
|
+
/**
|
|
30
|
+
* If called, will re-initialize the editor with the collaboration extension removed.
|
|
31
|
+
* This will prevent syncing back deletions of content not present in the current schema.
|
|
32
|
+
*/
|
|
33
|
+
disableCollaboration: () => void;
|
|
34
|
+
};
|
|
26
35
|
update: {
|
|
27
36
|
editor: Editor;
|
|
28
37
|
transaction: Transaction;
|
|
@@ -66,8 +75,20 @@ export interface EditorOptions {
|
|
|
66
75
|
enableInputRules: EnableRules;
|
|
67
76
|
enablePasteRules: EnableRules;
|
|
68
77
|
enableCoreExtensions: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* If `true`, the editor will check the content for errors on initialization.
|
|
80
|
+
* Emitting the `contentError` event if the content is invalid.
|
|
81
|
+
* Which can be used to show a warning or error message to the user.
|
|
82
|
+
* @default false
|
|
83
|
+
*/
|
|
84
|
+
enableContentCheck: boolean;
|
|
69
85
|
onBeforeCreate: (props: EditorEvents['beforeCreate']) => void;
|
|
70
86
|
onCreate: (props: EditorEvents['create']) => void;
|
|
87
|
+
/**
|
|
88
|
+
* Called when the editor encounters an error while parsing the content.
|
|
89
|
+
* Only enabled if `enableContentCheck` is `true`.
|
|
90
|
+
*/
|
|
91
|
+
onContentError: (props: EditorEvents['contentError']) => void;
|
|
71
92
|
onUpdate: (props: EditorEvents['update']) => void;
|
|
72
93
|
onSelectionUpdate: (props: EditorEvents['selectionUpdate']) => void;
|
|
73
94
|
onTransaction: (props: EditorEvents['transaction']) => void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiptap/core",
|
|
3
3
|
"description": "headless rich text editor",
|
|
4
|
-
"version": "2.5.0-beta.
|
|
4
|
+
"version": "2.5.0-beta.1",
|
|
5
5
|
"homepage": "https://tiptap.dev",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"tiptap",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"dist"
|
|
33
33
|
],
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@tiptap/pm": "^2.5.0-beta.
|
|
35
|
+
"@tiptap/pm": "^2.5.0-beta.1"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@tiptap/pm": "^2.0.0"
|
package/src/Editor.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
MarkType,
|
|
2
|
+
MarkType,
|
|
3
|
+
Node as ProseMirrorNode,
|
|
4
|
+
NodeType,
|
|
5
|
+
Schema,
|
|
3
6
|
} from '@tiptap/pm/model'
|
|
4
7
|
import {
|
|
5
8
|
EditorState, Plugin, PluginKey, Transaction,
|
|
@@ -36,8 +39,10 @@ import { isFunction } from './utilities/isFunction.js'
|
|
|
36
39
|
|
|
37
40
|
export * as extensions from './extensions/index.js'
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
declare global {
|
|
43
|
+
interface HTMLElement {
|
|
44
|
+
editor?: Editor;
|
|
45
|
+
}
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
export class Editor extends EventEmitter<EditorEvents> {
|
|
@@ -69,6 +74,7 @@ export class Editor extends EventEmitter<EditorEvents> {
|
|
|
69
74
|
enableInputRules: true,
|
|
70
75
|
enablePasteRules: true,
|
|
71
76
|
enableCoreExtensions: true,
|
|
77
|
+
enableContentCheck: false,
|
|
72
78
|
onBeforeCreate: () => null,
|
|
73
79
|
onCreate: () => null,
|
|
74
80
|
onUpdate: () => null,
|
|
@@ -77,6 +83,7 @@ export class Editor extends EventEmitter<EditorEvents> {
|
|
|
77
83
|
onFocus: () => null,
|
|
78
84
|
onBlur: () => null,
|
|
79
85
|
onDestroy: () => null,
|
|
86
|
+
onContentError: ({ error }) => { throw error },
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
constructor(options: Partial<EditorOptions> = {}) {
|
|
@@ -87,6 +94,7 @@ export class Editor extends EventEmitter<EditorEvents> {
|
|
|
87
94
|
this.createSchema()
|
|
88
95
|
this.on('beforeCreate', this.options.onBeforeCreate)
|
|
89
96
|
this.emit('beforeCreate', { editor: this })
|
|
97
|
+
this.on('contentError', this.options.onContentError)
|
|
90
98
|
this.createView()
|
|
91
99
|
this.injectCSS()
|
|
92
100
|
this.on('create', this.options.onCreate)
|
|
@@ -276,7 +284,40 @@ export class Editor extends EventEmitter<EditorEvents> {
|
|
|
276
284
|
* Creates a ProseMirror view.
|
|
277
285
|
*/
|
|
278
286
|
private createView(): void {
|
|
279
|
-
|
|
287
|
+
let doc: ProseMirrorNode
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
doc = createDocument(
|
|
291
|
+
this.options.content,
|
|
292
|
+
this.schema,
|
|
293
|
+
this.options.parseOptions,
|
|
294
|
+
{ errorOnInvalidContent: this.options.enableContentCheck },
|
|
295
|
+
)
|
|
296
|
+
} catch (e) {
|
|
297
|
+
if (!(e instanceof Error) || !['[tiptap error]: Invalid JSON content', '[tiptap error]: Invalid HTML content'].includes(e.message)) {
|
|
298
|
+
// Not the content error we were expecting
|
|
299
|
+
throw e
|
|
300
|
+
}
|
|
301
|
+
this.emit('contentError', {
|
|
302
|
+
editor: this,
|
|
303
|
+
error: e as Error,
|
|
304
|
+
disableCollaboration: () => {
|
|
305
|
+
// To avoid syncing back invalid content, reinitialize the extensions without the collaboration extension
|
|
306
|
+
this.options.extensions = this.options.extensions.filter(extension => extension.name !== 'collaboration')
|
|
307
|
+
|
|
308
|
+
// Restart the initialization process by recreating the extension manager with the new set of extensions
|
|
309
|
+
this.createExtensionManager()
|
|
310
|
+
},
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// Content is invalid, but attempt to create it anyway, stripping out the invalid parts
|
|
314
|
+
doc = createDocument(
|
|
315
|
+
this.options.content,
|
|
316
|
+
this.schema,
|
|
317
|
+
this.options.parseOptions,
|
|
318
|
+
{ errorOnInvalidContent: false },
|
|
319
|
+
)
|
|
320
|
+
}
|
|
280
321
|
const selection = resolveFocusPosition(doc, this.options.autofocus)
|
|
281
322
|
|
|
282
323
|
this.view = new EditorView(this.options.element, {
|
|
@@ -35,8 +35,21 @@ declare module '@tiptap/core' {
|
|
|
35
35
|
* Whether to update the selection after inserting the content.
|
|
36
36
|
*/
|
|
37
37
|
updateSelection?: boolean
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether to apply input rules after inserting the content.
|
|
41
|
+
*/
|
|
38
42
|
applyInputRules?: boolean
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Whether to apply paste rules after inserting the content.
|
|
46
|
+
*/
|
|
39
47
|
applyPasteRules?: boolean
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether to throw an error if the content is invalid.
|
|
51
|
+
*/
|
|
52
|
+
errorOnInvalidContent?: boolean
|
|
40
53
|
},
|
|
41
54
|
) => ReturnType
|
|
42
55
|
}
|
|
@@ -57,12 +70,19 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
|
|
|
57
70
|
...options,
|
|
58
71
|
}
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
let content: Fragment | ProseMirrorNode
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
content = createNodeFromContent(value, editor.schema, {
|
|
77
|
+
parseOptions: {
|
|
78
|
+
preserveWhitespace: 'full',
|
|
79
|
+
...options.parseOptions,
|
|
80
|
+
},
|
|
81
|
+
errorOnInvalidContent: options.errorOnInvalidContent ?? editor.options.enableContentCheck,
|
|
82
|
+
})
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
66
86
|
|
|
67
87
|
// don’t dispatch an empty fragment because this can lead to strange errors
|
|
68
88
|
if (content.toString() === '<>') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ParseOptions } from '@tiptap/pm/model'
|
|
1
|
+
import { Fragment, Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm/model'
|
|
2
2
|
|
|
3
3
|
import { createDocument } from '../helpers/createDocument.js'
|
|
4
4
|
import { Content, RawCommands } from '../types.js'
|
|
@@ -30,14 +30,32 @@ declare module '@tiptap/core' {
|
|
|
30
30
|
* @default {}
|
|
31
31
|
*/
|
|
32
32
|
parseOptions?: ParseOptions,
|
|
33
|
+
/**
|
|
34
|
+
* Options for `setContent`.
|
|
35
|
+
*/
|
|
36
|
+
options?: {
|
|
37
|
+
/**
|
|
38
|
+
* Whether to throw an error if the content is invalid.
|
|
39
|
+
*/
|
|
40
|
+
errorOnInvalidContent?: boolean
|
|
41
|
+
},
|
|
33
42
|
) => ReturnType
|
|
34
43
|
}
|
|
35
44
|
}
|
|
36
45
|
}
|
|
37
46
|
|
|
38
|
-
export const setContent: RawCommands['setContent'] = (content, emitUpdate = false, parseOptions = {}) => ({ tr, editor, dispatch }) => {
|
|
47
|
+
export const setContent: RawCommands['setContent'] = (content, emitUpdate = false, parseOptions = {}, options = {}) => ({ tr, editor, dispatch }) => {
|
|
39
48
|
const { doc } = tr
|
|
40
|
-
|
|
49
|
+
|
|
50
|
+
let document: Fragment | ProseMirrorNode
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
document = createDocument(content, editor.schema, parseOptions, {
|
|
54
|
+
errorOnInvalidContent: options.errorOnInvalidContent ?? editor.options.enableContentCheck,
|
|
55
|
+
})
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
41
59
|
|
|
42
60
|
if (dispatch) {
|
|
43
61
|
tr.replaceWith(0, doc.content.size, document).setMeta('preventUpdate', !emitUpdate)
|
|
@@ -14,6 +14,11 @@ export function createDocument(
|
|
|
14
14
|
content: Content,
|
|
15
15
|
schema: Schema,
|
|
16
16
|
parseOptions: ParseOptions = {},
|
|
17
|
+
options: { errorOnInvalidContent?: boolean } = {},
|
|
17
18
|
): ProseMirrorNode {
|
|
18
|
-
return createNodeFromContent(content, schema, {
|
|
19
|
+
return createNodeFromContent(content, schema, {
|
|
20
|
+
slice: false,
|
|
21
|
+
parseOptions,
|
|
22
|
+
errorOnInvalidContent: options.errorOnInvalidContent,
|
|
23
|
+
}) as ProseMirrorNode
|
|
19
24
|
}
|
|
@@ -12,6 +12,7 @@ import { elementFromString } from '../utilities/elementFromString.js'
|
|
|
12
12
|
export type CreateNodeFromContentOptions = {
|
|
13
13
|
slice?: boolean
|
|
14
14
|
parseOptions?: ParseOptions
|
|
15
|
+
errorOnInvalidContent?: boolean
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -46,6 +47,10 @@ export function createNodeFromContent(
|
|
|
46
47
|
|
|
47
48
|
return schema.nodeFromJSON(content)
|
|
48
49
|
} catch (error) {
|
|
50
|
+
if (options.errorOnInvalidContent) {
|
|
51
|
+
throw new Error('[tiptap error]: Invalid JSON content', { cause: error as Error })
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
console.warn('[tiptap warn]: Invalid content.', 'Passed value:', content, 'Error:', error)
|
|
50
55
|
|
|
51
56
|
return createNodeFromContent('', schema, options)
|
|
@@ -53,11 +58,46 @@ export function createNodeFromContent(
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
if (isTextContent) {
|
|
56
|
-
|
|
61
|
+
let schemaToUse = schema
|
|
62
|
+
let hasInvalidContent = false
|
|
63
|
+
|
|
64
|
+
// Only ever check for invalid content if we're supposed to throw an error
|
|
65
|
+
if (options.errorOnInvalidContent) {
|
|
66
|
+
schemaToUse = new Schema({
|
|
67
|
+
topNode: schema.spec.topNode,
|
|
68
|
+
marks: schema.spec.marks,
|
|
69
|
+
// Prosemirror's schemas are executed such that: the last to execute, matches last
|
|
70
|
+
// This means that we can add a catch-all node at the end of the schema to catch any content that we don't know how to handle
|
|
71
|
+
nodes: schema.spec.nodes.append({
|
|
72
|
+
__tiptap__private__unknown__catch__all__node: {
|
|
73
|
+
content: 'inline*',
|
|
74
|
+
group: 'block',
|
|
75
|
+
parseDOM: [
|
|
76
|
+
{
|
|
77
|
+
tag: '*',
|
|
78
|
+
getAttrs: () => {
|
|
79
|
+
// If this is ever called, we know that the content has something that we don't know how to handle in the schema
|
|
80
|
+
hasInvalidContent = true
|
|
81
|
+
return null
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
})
|
|
88
|
+
}
|
|
57
89
|
|
|
58
|
-
|
|
90
|
+
const parser = DOMParser.fromSchema(schemaToUse)
|
|
91
|
+
|
|
92
|
+
const response = options.slice
|
|
59
93
|
? parser.parseSlice(elementFromString(content), options.parseOptions).content
|
|
60
94
|
: parser.parse(elementFromString(content), options.parseOptions)
|
|
95
|
+
|
|
96
|
+
if (options.errorOnInvalidContent && hasInvalidContent) {
|
|
97
|
+
throw new Error('[tiptap error]: Invalid HTML content')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return response
|
|
61
101
|
}
|
|
62
102
|
|
|
63
103
|
return createNodeFromContent('', schema, options)
|
package/src/types.ts
CHANGED
|
@@ -39,6 +39,15 @@ export type MaybeThisParameterType<T> = Exclude<T, Primitive> extends (...args:
|
|
|
39
39
|
export interface EditorEvents {
|
|
40
40
|
beforeCreate: { editor: Editor }
|
|
41
41
|
create: { editor: Editor }
|
|
42
|
+
contentError: {
|
|
43
|
+
editor: Editor,
|
|
44
|
+
error: Error,
|
|
45
|
+
/**
|
|
46
|
+
* If called, will re-initialize the editor with the collaboration extension removed.
|
|
47
|
+
* This will prevent syncing back deletions of content not present in the current schema.
|
|
48
|
+
*/
|
|
49
|
+
disableCollaboration: () => void
|
|
50
|
+
}
|
|
42
51
|
update: { editor: Editor; transaction: Transaction }
|
|
43
52
|
selectionUpdate: { editor: Editor; transaction: Transaction }
|
|
44
53
|
transaction: { editor: Editor; transaction: Transaction }
|
|
@@ -67,8 +76,20 @@ export interface EditorOptions {
|
|
|
67
76
|
enableInputRules: EnableRules
|
|
68
77
|
enablePasteRules: EnableRules
|
|
69
78
|
enableCoreExtensions: boolean
|
|
79
|
+
/**
|
|
80
|
+
* If `true`, the editor will check the content for errors on initialization.
|
|
81
|
+
* Emitting the `contentError` event if the content is invalid.
|
|
82
|
+
* Which can be used to show a warning or error message to the user.
|
|
83
|
+
* @default false
|
|
84
|
+
*/
|
|
85
|
+
enableContentCheck: boolean
|
|
70
86
|
onBeforeCreate: (props: EditorEvents['beforeCreate']) => void
|
|
71
87
|
onCreate: (props: EditorEvents['create']) => void
|
|
88
|
+
/**
|
|
89
|
+
* Called when the editor encounters an error while parsing the content.
|
|
90
|
+
* Only enabled if `enableContentCheck` is `true`.
|
|
91
|
+
*/
|
|
92
|
+
onContentError: (props: EditorEvents['contentError']) => void
|
|
72
93
|
onUpdate: (props: EditorEvents['update']) => void
|
|
73
94
|
onSelectionUpdate: (props: EditorEvents['selectionUpdate']) => void
|
|
74
95
|
onTransaction: (props: EditorEvents['transaction']) => void
|