@prosekit/core 0.8.2 → 0.8.4
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/editor-CfkZ4TNU.d.ts +748 -0
- package/dist/editor-CfkZ4TNU.d.ts.map +1 -0
- package/dist/{editor-DbMrpnmL.js → editor-CizSwUN8.js} +102 -192
- package/dist/editor-CizSwUN8.js.map +1 -0
- package/dist/prosekit-core-test.d.ts +20 -19
- package/dist/prosekit-core-test.d.ts.map +1 -0
- package/dist/prosekit-core-test.js +4 -5
- package/dist/prosekit-core-test.js.map +1 -0
- package/dist/prosekit-core.d.ts +782 -757
- package/dist/prosekit-core.d.ts.map +1 -0
- package/dist/prosekit-core.js +30 -45
- package/dist/prosekit-core.js.map +1 -0
- package/package.json +14 -11
- package/src/commands/add-mark.ts +53 -0
- package/src/commands/expand-mark.ts +96 -0
- package/src/commands/insert-default-block.spec.ts +102 -0
- package/src/commands/insert-default-block.ts +49 -0
- package/src/commands/insert-node.ts +71 -0
- package/src/commands/insert-text.ts +24 -0
- package/src/commands/remove-mark.ts +54 -0
- package/src/commands/remove-node.ts +43 -0
- package/src/commands/select-all.ts +16 -0
- package/src/commands/set-block-type.ts +64 -0
- package/src/commands/set-node-attrs.ts +68 -0
- package/src/commands/toggle-mark.ts +65 -0
- package/src/commands/toggle-node.ts +47 -0
- package/src/commands/toggle-wrap.spec.ts +35 -0
- package/src/commands/toggle-wrap.ts +42 -0
- package/src/commands/unset-block-type.spec.ts +49 -0
- package/src/commands/unset-block-type.ts +84 -0
- package/src/commands/unset-mark.spec.ts +35 -0
- package/src/commands/unset-mark.ts +38 -0
- package/src/commands/wrap.ts +50 -0
- package/src/editor/action.spec.ts +143 -0
- package/src/editor/action.ts +248 -0
- package/src/editor/editor.spec.ts +186 -0
- package/src/editor/editor.ts +563 -0
- package/src/editor/union.spec.ts +108 -0
- package/src/editor/union.ts +47 -0
- package/src/editor/with-priority.ts +25 -0
- package/src/error.ts +28 -0
- package/src/extensions/clipboard-serializer.ts +107 -0
- package/src/extensions/command.ts +121 -0
- package/src/extensions/default-state.spec.ts +60 -0
- package/src/extensions/default-state.ts +76 -0
- package/src/extensions/doc.ts +31 -0
- package/src/extensions/events/doc-change.ts +34 -0
- package/src/extensions/events/dom-event.spec.ts +70 -0
- package/src/extensions/events/dom-event.ts +117 -0
- package/src/extensions/events/editor-event.ts +293 -0
- package/src/extensions/events/focus.spec.ts +50 -0
- package/src/extensions/events/focus.ts +28 -0
- package/src/extensions/events/plugin-view.ts +132 -0
- package/src/extensions/history.ts +81 -0
- package/src/extensions/keymap-base.ts +60 -0
- package/src/extensions/keymap.spec.ts +89 -0
- package/src/extensions/keymap.ts +96 -0
- package/src/extensions/mark-spec.spec.ts +177 -0
- package/src/extensions/mark-spec.ts +181 -0
- package/src/extensions/mark-view-effect.ts +85 -0
- package/src/extensions/mark-view.ts +43 -0
- package/src/extensions/node-spec.spec.ts +224 -0
- package/src/extensions/node-spec.ts +199 -0
- package/src/extensions/node-view-effect.ts +85 -0
- package/src/extensions/node-view.ts +43 -0
- package/src/extensions/paragraph.ts +61 -0
- package/src/extensions/plugin.ts +91 -0
- package/src/extensions/text.ts +34 -0
- package/src/facets/base-extension.ts +54 -0
- package/src/facets/command.ts +21 -0
- package/src/facets/facet-extension.spec.ts +173 -0
- package/src/facets/facet-extension.ts +53 -0
- package/src/facets/facet-node.spec.ts +265 -0
- package/src/facets/facet-node.ts +185 -0
- package/src/facets/facet-types.ts +9 -0
- package/src/facets/facet.spec.ts +76 -0
- package/src/facets/facet.ts +84 -0
- package/src/facets/root.ts +44 -0
- package/src/facets/schema-spec.ts +30 -0
- package/src/facets/schema.ts +26 -0
- package/src/facets/state.ts +57 -0
- package/src/facets/union-extension.ts +41 -0
- package/src/index.ts +302 -0
- package/src/test/index.ts +4 -0
- package/src/test/test-builder.ts +68 -0
- package/src/test/test-editor.spec.ts +104 -0
- package/src/test/test-editor.ts +113 -0
- package/src/testing/index.ts +283 -0
- package/src/testing/keyboard.ts +5 -0
- package/src/types/any-function.ts +4 -0
- package/src/types/assert-type-equal.ts +8 -0
- package/src/types/attrs.ts +32 -0
- package/src/types/base-node-view-options.ts +33 -0
- package/src/types/dom-node.ts +1 -0
- package/src/types/extension-command.ts +52 -0
- package/src/types/extension-mark.ts +15 -0
- package/src/types/extension-node.ts +15 -0
- package/src/types/extension.spec.ts +56 -0
- package/src/types/extension.ts +168 -0
- package/src/types/model.ts +54 -0
- package/src/types/object-entries.ts +13 -0
- package/src/types/pick-string-literal.spec.ts +10 -0
- package/src/types/pick-string-literal.ts +6 -0
- package/src/types/pick-sub-type.spec.ts +20 -0
- package/src/types/pick-sub-type.ts +6 -0
- package/src/types/priority.ts +12 -0
- package/src/types/setter.ts +4 -0
- package/src/types/simplify-deeper.spec.ts +40 -0
- package/src/types/simplify-deeper.ts +6 -0
- package/src/types/simplify-union.spec.ts +21 -0
- package/src/types/simplify-union.ts +11 -0
- package/src/utils/array-grouping.spec.ts +29 -0
- package/src/utils/array-grouping.ts +25 -0
- package/src/utils/array.ts +21 -0
- package/src/utils/assert.ts +13 -0
- package/src/utils/attrs-match.ts +20 -0
- package/src/utils/can-use-regex-lookbehind.ts +12 -0
- package/src/utils/clsx.spec.ts +14 -0
- package/src/utils/clsx.ts +12 -0
- package/src/utils/collect-children.ts +21 -0
- package/src/utils/collect-nodes.ts +37 -0
- package/src/utils/combine-event-handlers.spec.ts +27 -0
- package/src/utils/combine-event-handlers.ts +27 -0
- package/src/utils/contains-inline-node.ts +17 -0
- package/src/utils/deep-equals.spec.ts +26 -0
- package/src/utils/deep-equals.ts +29 -0
- package/src/utils/default-block-at.ts +15 -0
- package/src/utils/editor-content.spec.ts +47 -0
- package/src/utils/editor-content.ts +77 -0
- package/src/utils/env.ts +6 -0
- package/src/utils/find-parent-node-of-type.ts +29 -0
- package/src/utils/find-parent-node.spec.ts +68 -0
- package/src/utils/find-parent-node.ts +55 -0
- package/src/utils/get-custom-selection.ts +19 -0
- package/src/utils/get-dom-api.ts +56 -0
- package/src/utils/get-id.spec.ts +14 -0
- package/src/utils/get-id.ts +13 -0
- package/src/utils/get-mark-type.ts +20 -0
- package/src/utils/get-node-type.ts +20 -0
- package/src/utils/get-node-types.ts +19 -0
- package/src/utils/includes-mark.ts +18 -0
- package/src/utils/is-at-block-start.ts +26 -0
- package/src/utils/is-in-code-block.ts +18 -0
- package/src/utils/is-mark-absent.spec.ts +53 -0
- package/src/utils/is-mark-absent.ts +42 -0
- package/src/utils/is-mark-active.ts +27 -0
- package/src/utils/is-node-active.ts +25 -0
- package/src/utils/is-subset.spec.ts +12 -0
- package/src/utils/is-subset.ts +11 -0
- package/src/utils/maybe-run.spec.ts +39 -0
- package/src/utils/maybe-run.ts +11 -0
- package/src/utils/merge-objects.spec.ts +30 -0
- package/src/utils/merge-objects.ts +11 -0
- package/src/utils/merge-specs.ts +35 -0
- package/src/utils/object-equal.spec.ts +26 -0
- package/src/utils/object-equal.ts +28 -0
- package/src/utils/output-spec.test.ts +95 -0
- package/src/utils/output-spec.ts +130 -0
- package/src/utils/parse.spec.ts +46 -0
- package/src/utils/parse.ts +321 -0
- package/src/utils/remove-undefined-values.spec.ts +15 -0
- package/src/utils/remove-undefined-values.ts +9 -0
- package/src/utils/set-selection-around.ts +11 -0
- package/src/utils/type-assertion.ts +91 -0
- package/src/utils/unicode.spec.ts +10 -0
- package/src/utils/unicode.ts +4 -0
- package/src/utils/with-skip-code-block.ts +15 -0
- package/dist/editor-CjVyjJqw.d.ts +0 -739
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProseMirrorNode,
|
|
3
|
+
Schema,
|
|
4
|
+
} from '@prosekit/pm/model'
|
|
5
|
+
import {
|
|
6
|
+
EditorState,
|
|
7
|
+
type Command,
|
|
8
|
+
type Plugin,
|
|
9
|
+
type Selection,
|
|
10
|
+
type Transaction,
|
|
11
|
+
} from '@prosekit/pm/state'
|
|
12
|
+
import {
|
|
13
|
+
EditorView,
|
|
14
|
+
type DirectEditorProps,
|
|
15
|
+
type EditorProps,
|
|
16
|
+
} from '@prosekit/pm/view'
|
|
17
|
+
|
|
18
|
+
import { ProseKitError } from '../error'
|
|
19
|
+
import { defineDefaultState } from '../extensions/default-state'
|
|
20
|
+
import type { BaseExtension } from '../facets/base-extension'
|
|
21
|
+
import {
|
|
22
|
+
subtractFacetNode,
|
|
23
|
+
unionFacetNode,
|
|
24
|
+
type FacetNode,
|
|
25
|
+
} from '../facets/facet-node'
|
|
26
|
+
import type {
|
|
27
|
+
Extension,
|
|
28
|
+
ExtractCommandActions,
|
|
29
|
+
ExtractMarkActions,
|
|
30
|
+
ExtractMarkNames,
|
|
31
|
+
ExtractNodeActions,
|
|
32
|
+
ExtractNodeNames,
|
|
33
|
+
} from '../types/extension'
|
|
34
|
+
import type {
|
|
35
|
+
CommandAction,
|
|
36
|
+
CommandCreator,
|
|
37
|
+
} from '../types/extension-command'
|
|
38
|
+
import type {
|
|
39
|
+
NodeJSON,
|
|
40
|
+
SelectionJSON,
|
|
41
|
+
} from '../types/model'
|
|
42
|
+
import { assert } from '../utils/assert'
|
|
43
|
+
import { deepEquals } from '../utils/deep-equals'
|
|
44
|
+
import {
|
|
45
|
+
getEditorContentDoc,
|
|
46
|
+
getEditorSelection,
|
|
47
|
+
} from '../utils/editor-content'
|
|
48
|
+
import {
|
|
49
|
+
htmlFromNode,
|
|
50
|
+
jsonFromNode,
|
|
51
|
+
type DOMDocumentOptions,
|
|
52
|
+
} from '../utils/parse'
|
|
53
|
+
|
|
54
|
+
import {
|
|
55
|
+
createMarkActions,
|
|
56
|
+
createNodeActions,
|
|
57
|
+
type MarkAction,
|
|
58
|
+
type NodeAction,
|
|
59
|
+
} from './action'
|
|
60
|
+
import { union } from './union'
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
export interface EditorOptions<E extends Extension> {
|
|
66
|
+
/**
|
|
67
|
+
* The extension to use when creating the editor.
|
|
68
|
+
*/
|
|
69
|
+
extension: E
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The starting document to use when creating the editor. It can be a
|
|
73
|
+
* ProseMirror node JSON object, a HTML string, or a HTML element instance.
|
|
74
|
+
*/
|
|
75
|
+
defaultContent?: NodeJSON | string | HTMLElement
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A JSON object representing the starting document to use when creating the
|
|
79
|
+
* editor.
|
|
80
|
+
*
|
|
81
|
+
* @deprecated Use `defaultContent` instead.
|
|
82
|
+
*/
|
|
83
|
+
defaultDoc?: NodeJSON
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A HTML element or a HTML string representing the starting document to use
|
|
87
|
+
* when creating the editor.
|
|
88
|
+
*
|
|
89
|
+
* @deprecated Use `defaultContent` instead.
|
|
90
|
+
*/
|
|
91
|
+
defaultHTML?: string | HTMLElement
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A JSON object representing the starting selection to use when creating the
|
|
95
|
+
* editor. It's only used when `defaultContent` is also provided.
|
|
96
|
+
*/
|
|
97
|
+
defaultSelection?: SelectionJSON
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @public
|
|
102
|
+
*/
|
|
103
|
+
export interface getDocHTMLOptions extends DOMDocumentOptions {}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @internal
|
|
107
|
+
*/
|
|
108
|
+
export function setupEditorExtension<E extends Extension>(
|
|
109
|
+
options: EditorOptions<E>,
|
|
110
|
+
): E {
|
|
111
|
+
if (options.defaultContent || options.defaultDoc || options.defaultHTML) {
|
|
112
|
+
return union(
|
|
113
|
+
options.extension,
|
|
114
|
+
defineDefaultState(options),
|
|
115
|
+
) as Extension as E
|
|
116
|
+
}
|
|
117
|
+
return options.extension
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @public
|
|
122
|
+
*/
|
|
123
|
+
export function createEditor<E extends Extension>(
|
|
124
|
+
options: EditorOptions<E>,
|
|
125
|
+
): Editor<E> {
|
|
126
|
+
const extension = setupEditorExtension(options)
|
|
127
|
+
const instance = new EditorInstance(extension)
|
|
128
|
+
return new Editor(instance)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* An internal class to make TypeScript generic type easier to use.
|
|
133
|
+
*
|
|
134
|
+
* @internal
|
|
135
|
+
*/
|
|
136
|
+
export class EditorInstance {
|
|
137
|
+
view: EditorView | null = null
|
|
138
|
+
schema: Schema
|
|
139
|
+
nodes: Record<string, NodeAction>
|
|
140
|
+
marks: Record<string, MarkAction>
|
|
141
|
+
commands: Record<string, CommandAction> = {}
|
|
142
|
+
|
|
143
|
+
private tree: FacetNode
|
|
144
|
+
private directEditorProps: DirectEditorProps
|
|
145
|
+
private afterMounted: Array<VoidFunction> = []
|
|
146
|
+
|
|
147
|
+
constructor(extension: Extension) {
|
|
148
|
+
this.tree = (extension as BaseExtension).getTree()
|
|
149
|
+
|
|
150
|
+
const payload = this.tree.getRootOutput()
|
|
151
|
+
const schema = payload.schema
|
|
152
|
+
const stateConfig = payload.state
|
|
153
|
+
|
|
154
|
+
assert(schema && stateConfig, 'Schema must be defined')
|
|
155
|
+
|
|
156
|
+
const state = EditorState.create(stateConfig)
|
|
157
|
+
|
|
158
|
+
if (payload.commands) {
|
|
159
|
+
for (const [name, commandCreator] of Object.entries(payload.commands)) {
|
|
160
|
+
this.defineCommand(name, commandCreator)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.nodes = createNodeActions(state.schema, this.getState)
|
|
165
|
+
this.marks = createMarkActions(state.schema, this.getState)
|
|
166
|
+
|
|
167
|
+
this.schema = state.schema
|
|
168
|
+
this.directEditorProps = { state, ...payload.view }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public getState = (): EditorState => {
|
|
172
|
+
return this.view?.state || this.directEditorProps.state
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private getDoc(): ProseMirrorNode {
|
|
176
|
+
return this.getState().doc
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private getProp<PropName extends keyof EditorProps>(propName: PropName): EditorProps[PropName] | undefined {
|
|
180
|
+
return this.view?.someProp(propName) ?? this.directEditorProps[propName]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public updateState(state: EditorState): void {
|
|
184
|
+
if (this.view) {
|
|
185
|
+
this.view.updateState(state)
|
|
186
|
+
} else {
|
|
187
|
+
this.directEditorProps.state = state
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private dispatch = (tr: Transaction): void => {
|
|
192
|
+
if (this.view) {
|
|
193
|
+
this.view.dispatch(tr)
|
|
194
|
+
} else {
|
|
195
|
+
this.directEditorProps.state = this.directEditorProps.state.apply(tr)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public setContent(
|
|
200
|
+
content: NodeJSON | string | HTMLElement | ProseMirrorNode,
|
|
201
|
+
selection?: SelectionJSON | Selection | 'start' | 'end',
|
|
202
|
+
): void {
|
|
203
|
+
const doc = getEditorContentDoc(this.schema, content)
|
|
204
|
+
doc.check()
|
|
205
|
+
const sel = getEditorSelection(doc, selection || 'start')
|
|
206
|
+
|
|
207
|
+
const oldState = this.getState()
|
|
208
|
+
if (doc.eq(oldState.doc) && (!selection || sel.eq(oldState.selection))) {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const newState = EditorState.create({
|
|
213
|
+
doc,
|
|
214
|
+
selection: sel,
|
|
215
|
+
plugins: oldState.plugins,
|
|
216
|
+
})
|
|
217
|
+
this.updateState(newState)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Return a JSON object representing the editor's current document.
|
|
222
|
+
*/
|
|
223
|
+
public getDocJSON = (): NodeJSON => {
|
|
224
|
+
const state = this.getState()
|
|
225
|
+
return jsonFromNode(state.doc)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Return a HTML string representing the editor's current document.
|
|
230
|
+
*/
|
|
231
|
+
public getDocHTML = (options?: getDocHTMLOptions): string => {
|
|
232
|
+
const serializer = this.getProp('clipboardSerializer')
|
|
233
|
+
const DOMSerializer = serializer ? { fromSchema: () => serializer } : undefined
|
|
234
|
+
const doc = this.getDoc()
|
|
235
|
+
return htmlFromNode(doc, { ...options, DOMSerializer })
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private updateExtension(extension: Extension, add: boolean): void {
|
|
239
|
+
const view = this.view
|
|
240
|
+
|
|
241
|
+
// Don't update the extension if the editor is already unmounted
|
|
242
|
+
if (!view || view.isDestroyed) {
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const tree = (extension as BaseExtension).getTree()
|
|
247
|
+
const payload = tree.getRootOutput()
|
|
248
|
+
|
|
249
|
+
if (payload?.schema) {
|
|
250
|
+
throw new ProseKitError('Schema cannot be changed')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (payload?.view) {
|
|
254
|
+
throw new ProseKitError('View cannot be changed')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const oldPayload = this.tree.getRootOutput()
|
|
258
|
+
const oldPlugins = [...(view.state?.plugins ?? [])]
|
|
259
|
+
|
|
260
|
+
this.tree = add
|
|
261
|
+
? unionFacetNode(this.tree, tree)
|
|
262
|
+
: subtractFacetNode(this.tree, tree)
|
|
263
|
+
|
|
264
|
+
const newPayload = this.tree.getRootOutput()
|
|
265
|
+
const newPlugins = [...(newPayload?.state?.plugins ?? [])]
|
|
266
|
+
|
|
267
|
+
if (!deepEquals(oldPlugins, newPlugins)) {
|
|
268
|
+
const state = view.state.reconfigure({ plugins: newPlugins })
|
|
269
|
+
view.updateState(state)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (
|
|
273
|
+
newPayload?.commands
|
|
274
|
+
&& !deepEquals(oldPayload?.commands, newPayload?.commands)
|
|
275
|
+
) {
|
|
276
|
+
const commands = newPayload.commands
|
|
277
|
+
const names = Object.keys(commands)
|
|
278
|
+
for (const name of names) {
|
|
279
|
+
this.defineCommand(name, commands[name])
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
public use(extension: Extension): VoidFunction {
|
|
285
|
+
if (!this.mounted) {
|
|
286
|
+
let canceled = false
|
|
287
|
+
let lazyRemove: VoidFunction | null = null
|
|
288
|
+
|
|
289
|
+
const lazyCreate = () => {
|
|
290
|
+
if (!canceled) {
|
|
291
|
+
lazyRemove = this.use(extension)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.afterMounted.push(lazyCreate)
|
|
296
|
+
|
|
297
|
+
return () => {
|
|
298
|
+
canceled = true
|
|
299
|
+
lazyRemove?.()
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this.updateExtension(extension, true)
|
|
304
|
+
return () => this.updateExtension(extension, false)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
public mount(place: HTMLElement): void {
|
|
308
|
+
if (this.view) {
|
|
309
|
+
throw new ProseKitError('Editor is already mounted')
|
|
310
|
+
}
|
|
311
|
+
this.view = new EditorView({ mount: place }, this.directEditorProps)
|
|
312
|
+
this.afterMounted.forEach((callback) => callback())
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
public unmount(): void {
|
|
316
|
+
// If the editor is not mounted, do nothing
|
|
317
|
+
if (!this.view) return
|
|
318
|
+
|
|
319
|
+
this.directEditorProps.state = this.view.state
|
|
320
|
+
this.view.destroy()
|
|
321
|
+
this.view = null
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
get mounted(): boolean {
|
|
325
|
+
return !!this.view && !this.view.isDestroyed
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
public get assertView(): EditorView {
|
|
329
|
+
if (!this.view) {
|
|
330
|
+
throw new ProseKitError('Editor is not mounted')
|
|
331
|
+
}
|
|
332
|
+
return this.view
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
public definePlugins(plugins: readonly Plugin[]): void {
|
|
336
|
+
const view = this.assertView
|
|
337
|
+
const state = view.state
|
|
338
|
+
const newPlugins = [...plugins, ...state.plugins]
|
|
339
|
+
const newState = state.reconfigure({ plugins: newPlugins })
|
|
340
|
+
view.setProps({ state: newState })
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
public removePlugins(plugins: readonly Plugin[]): void {
|
|
344
|
+
const view = this.view
|
|
345
|
+
if (!view) return
|
|
346
|
+
|
|
347
|
+
const state = view.state
|
|
348
|
+
const newPlugins = state.plugins.filter((p) => !plugins.includes(p))
|
|
349
|
+
const newState = state.reconfigure({ plugins: newPlugins })
|
|
350
|
+
view.setProps({ state: newState })
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
exec(command: Command): boolean {
|
|
354
|
+
const state = this.getState()
|
|
355
|
+
return command(state, this.dispatch, this.view ?? undefined)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
canExec(command: Command): boolean {
|
|
359
|
+
const state = this.getState()
|
|
360
|
+
return command(state, undefined, this.view ?? undefined)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
public defineCommand<Args extends any[] = any[]>(
|
|
364
|
+
name: string,
|
|
365
|
+
commandCreator: CommandCreator<Args>,
|
|
366
|
+
): void {
|
|
367
|
+
const action: CommandAction<Args> = (...args: Args) => {
|
|
368
|
+
const command = commandCreator(...args)
|
|
369
|
+
return this.exec(command)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const canExec = (...args: Args) => {
|
|
373
|
+
const command = commandCreator(...args)
|
|
374
|
+
return this.canExec(command)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
action.canApply = canExec
|
|
378
|
+
action.canExec = canExec
|
|
379
|
+
|
|
380
|
+
this.commands[name] = action as CommandAction
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
public removeCommand(name: string): void {
|
|
384
|
+
delete this.commands[name]
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* @public
|
|
390
|
+
*/
|
|
391
|
+
export class Editor<E extends Extension = any> {
|
|
392
|
+
private instance: EditorInstance
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* @internal
|
|
396
|
+
*/
|
|
397
|
+
constructor(instance: EditorInstance) {
|
|
398
|
+
if (!(instance instanceof EditorInstance)) {
|
|
399
|
+
throw new TypeError('Invalid EditorInstance')
|
|
400
|
+
}
|
|
401
|
+
this.instance = instance
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Whether the editor is mounted.
|
|
406
|
+
*/
|
|
407
|
+
get mounted(): boolean {
|
|
408
|
+
return this.instance.mounted
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* The editor view.
|
|
413
|
+
*/
|
|
414
|
+
get view(): EditorView {
|
|
415
|
+
return this.instance.assertView
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* The editor schema.
|
|
420
|
+
*/
|
|
421
|
+
get schema(): Schema<ExtractNodeNames<E>, ExtractMarkNames<E>> {
|
|
422
|
+
return this.instance.schema
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The editor's current state.
|
|
427
|
+
*/
|
|
428
|
+
get state(): EditorState {
|
|
429
|
+
return this.instance.getState()
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Whether the editor is focused.
|
|
434
|
+
*/
|
|
435
|
+
get focused(): boolean {
|
|
436
|
+
return this.instance.view?.hasFocus() ?? false
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Mount the editor to the given HTML element.
|
|
441
|
+
* Pass `null` or `undefined` to unmount the editor.
|
|
442
|
+
*/
|
|
443
|
+
mount = (place: HTMLElement | null | undefined): void => {
|
|
444
|
+
if (place) {
|
|
445
|
+
this.instance.mount(place)
|
|
446
|
+
} else {
|
|
447
|
+
this.instance.unmount()
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Unmount the editor. This is equivalent to `mount(null)`.
|
|
453
|
+
*/
|
|
454
|
+
unmount = (): void => {
|
|
455
|
+
this.instance.unmount()
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Focus the editor.
|
|
460
|
+
*/
|
|
461
|
+
focus = (): void => {
|
|
462
|
+
this.instance.view?.focus()
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Blur the editor.
|
|
467
|
+
*/
|
|
468
|
+
blur = (): void => {
|
|
469
|
+
this.instance.view?.dom.blur()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Register an extension to the editor. Return a function to unregister the
|
|
474
|
+
* extension.
|
|
475
|
+
*/
|
|
476
|
+
use = (extension: Extension): VoidFunction => {
|
|
477
|
+
return this.instance.use(extension)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Update the editor's state.
|
|
482
|
+
*
|
|
483
|
+
* @remarks
|
|
484
|
+
*
|
|
485
|
+
* This is an advanced method. Use it only if you have a specific reason to
|
|
486
|
+
* directly manipulate the editor's state.
|
|
487
|
+
*/
|
|
488
|
+
updateState = (state: EditorState): void => {
|
|
489
|
+
this.instance.updateState(state)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Update the editor's document and selection.
|
|
494
|
+
*
|
|
495
|
+
* @param content - The new document to set. It can be one of the following:
|
|
496
|
+
* - A ProseMirror node instance
|
|
497
|
+
* - A ProseMirror node JSON object
|
|
498
|
+
* - An HTML string
|
|
499
|
+
* - An HTML element instance
|
|
500
|
+
* @param selection - Optional. Specifies the new selection. It can be one of the following:
|
|
501
|
+
* - A ProseMirror selection instance
|
|
502
|
+
* - A ProseMirror selection JSON object
|
|
503
|
+
* - The string "start" (to set selection at the beginning, default value)
|
|
504
|
+
* - The string "end" (to set selection at the end)
|
|
505
|
+
*/
|
|
506
|
+
setContent = (
|
|
507
|
+
content: ProseMirrorNode | NodeJSON | string | HTMLElement,
|
|
508
|
+
selection?: SelectionJSON | Selection | 'start' | 'end',
|
|
509
|
+
): void => {
|
|
510
|
+
return this.instance.setContent(content, selection)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Return a JSON object representing the editor's current document.
|
|
515
|
+
*/
|
|
516
|
+
public getDocJSON = (): NodeJSON => {
|
|
517
|
+
return this.instance.getDocJSON()
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Return a HTML string representing the editor's current document.
|
|
522
|
+
*/
|
|
523
|
+
public getDocHTML = (options?: getDocHTMLOptions): string => {
|
|
524
|
+
return this.instance.getDocHTML(options)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Execute the given command. Return `true` if the command was successfully
|
|
529
|
+
* executed, otherwise `false`.
|
|
530
|
+
*/
|
|
531
|
+
exec = (command: Command): boolean => {
|
|
532
|
+
return this.instance.exec(command)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Check if the given command can be executed. Return `true` if the command
|
|
537
|
+
* can be executed, otherwise `false`.
|
|
538
|
+
*/
|
|
539
|
+
canExec = (command: Command): boolean => {
|
|
540
|
+
return this.instance.canExec(command)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* All {@link CommandAction}s defined by the editor.
|
|
545
|
+
*/
|
|
546
|
+
get commands(): ExtractCommandActions<E> {
|
|
547
|
+
return this.instance.commands as ExtractCommandActions<E>
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* All {@link NodeAction}s defined by the editor.
|
|
552
|
+
*/
|
|
553
|
+
get nodes(): ExtractNodeActions<E> {
|
|
554
|
+
return this.instance.nodes as ExtractNodeActions<E>
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* All {@link MarkAction}s defined by the editor.
|
|
559
|
+
*/
|
|
560
|
+
get marks(): ExtractMarkActions<E> {
|
|
561
|
+
return this.instance.marks as ExtractMarkActions<E>
|
|
562
|
+
}
|
|
563
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Attrs } from '@prosekit/pm/model'
|
|
2
|
+
import type { Command } from '@prosekit/pm/state'
|
|
3
|
+
import {
|
|
4
|
+
describe,
|
|
5
|
+
expectTypeOf,
|
|
6
|
+
it,
|
|
7
|
+
} from 'vitest'
|
|
8
|
+
|
|
9
|
+
import { defineCommands } from '../extensions/command'
|
|
10
|
+
import { defineMarkSpec } from '../extensions/mark-spec'
|
|
11
|
+
import { defineNodeSpec } from '../extensions/node-spec'
|
|
12
|
+
import { assertTypeEqual } from '../types/assert-type-equal'
|
|
13
|
+
import type { Extension } from '../types/extension'
|
|
14
|
+
import type { CommandCreator } from '../types/extension-command'
|
|
15
|
+
|
|
16
|
+
import { union } from './union'
|
|
17
|
+
|
|
18
|
+
describe('union', () => {
|
|
19
|
+
it('can merge one extension types', () => {
|
|
20
|
+
const input = [extension3]
|
|
21
|
+
const output = union(input)
|
|
22
|
+
type Outout = typeof output
|
|
23
|
+
type Expected = Extension<{
|
|
24
|
+
Nodes: { node3: Attrs }
|
|
25
|
+
Marks: never
|
|
26
|
+
Commands: never
|
|
27
|
+
}>
|
|
28
|
+
|
|
29
|
+
expectTypeOf(output).toEqualTypeOf<Expected>()
|
|
30
|
+
assertTypeEqual<Outout, Expected>(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('can merge an extension array', () => {
|
|
34
|
+
const output = union([
|
|
35
|
+
extension1,
|
|
36
|
+
extension2,
|
|
37
|
+
extension3,
|
|
38
|
+
extension4,
|
|
39
|
+
extension5,
|
|
40
|
+
])
|
|
41
|
+
type Outout = typeof output
|
|
42
|
+
type Expected = Extension<{
|
|
43
|
+
Nodes: {
|
|
44
|
+
node3: Attrs
|
|
45
|
+
node4: Attrs
|
|
46
|
+
}
|
|
47
|
+
Marks: {
|
|
48
|
+
mark5: { attr5: string }
|
|
49
|
+
}
|
|
50
|
+
Commands: {
|
|
51
|
+
command1: [{ arg1: string }]
|
|
52
|
+
command2: [{ arg2: number }]
|
|
53
|
+
command3: [number, boolean]
|
|
54
|
+
}
|
|
55
|
+
}>
|
|
56
|
+
|
|
57
|
+
expectTypeOf(output).toEqualTypeOf<Expected>()
|
|
58
|
+
assertTypeEqual<Outout, Expected>(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('can merge a nested array', () => {
|
|
62
|
+
const e12 = union(extension1, extension2)
|
|
63
|
+
const e34 = union(extension3, extension4)
|
|
64
|
+
const e12345 = union(e12, e34, extension5)
|
|
65
|
+
|
|
66
|
+
const input = union(e12345)
|
|
67
|
+
const output = union(input)
|
|
68
|
+
|
|
69
|
+
type Outout = typeof output
|
|
70
|
+
type Expected = Extension<{
|
|
71
|
+
Nodes: {
|
|
72
|
+
node3: Attrs
|
|
73
|
+
node4: Attrs
|
|
74
|
+
}
|
|
75
|
+
Marks: {
|
|
76
|
+
mark5: { attr5: string }
|
|
77
|
+
}
|
|
78
|
+
Commands: {
|
|
79
|
+
command1: [{ arg1: string }]
|
|
80
|
+
command2: [{ arg2: number }]
|
|
81
|
+
command3: [number, boolean]
|
|
82
|
+
}
|
|
83
|
+
}>
|
|
84
|
+
|
|
85
|
+
expectTypeOf(output).toEqualTypeOf<Expected>()
|
|
86
|
+
assertTypeEqual<Outout, Expected>(true)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
type Command1 = (args: { arg1: string }) => Command
|
|
91
|
+
|
|
92
|
+
const command1: Command1 = () => {
|
|
93
|
+
return () => true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function command2(_args: { arg2: number }): Command {
|
|
97
|
+
return () => true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const command3: CommandCreator<[number, boolean]> = (_num, _bool) => {
|
|
101
|
+
return () => true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const extension1 = defineCommands({ command1 })
|
|
105
|
+
const extension2 = defineCommands({ command2, command3 })
|
|
106
|
+
const extension3 = defineNodeSpec({ name: 'node3' })
|
|
107
|
+
const extension4 = defineNodeSpec({ name: 'node4' })
|
|
108
|
+
const extension5 = defineMarkSpec<'mark5', { attr5: string }>({ name: 'mark5' })
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { BaseExtension } from '../facets/base-extension'
|
|
2
|
+
import { UnionExtensionImpl } from '../facets/union-extension'
|
|
3
|
+
import type {
|
|
4
|
+
Extension,
|
|
5
|
+
Union,
|
|
6
|
+
} from '../types/extension'
|
|
7
|
+
import { assert } from '../utils/assert'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Merges multiple extensions into one. You can pass multiple extensions as
|
|
11
|
+
* arguments or a single array containing multiple extensions.
|
|
12
|
+
*
|
|
13
|
+
* @throws If no extensions are provided.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* function defineFancyNodes() {
|
|
19
|
+
* return union(
|
|
20
|
+
* defineFancyParagraph(),
|
|
21
|
+
* defineFancyHeading(),
|
|
22
|
+
* )
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* function defineFancyNodes() {
|
|
30
|
+
* return union([
|
|
31
|
+
* defineFancyParagraph(),
|
|
32
|
+
* defineFancyHeading(),
|
|
33
|
+
* ])
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
function union<const E extends readonly Extension[]>(...exts: E): Union<E>
|
|
40
|
+
function union<const E extends readonly Extension[]>(exts: E): Union<E>
|
|
41
|
+
function union(...exts: Array<Extension | Extension[]>): Extension {
|
|
42
|
+
const extensions: Extension[] = exts.flat()
|
|
43
|
+
assert(extensions.length > 0, 'At least one extension is required')
|
|
44
|
+
return new UnionExtensionImpl(extensions as BaseExtension[]) as Extension
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { union }
|