@prosekit/core 0.8.3 → 0.8.5
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-KZlceNQ1.d.ts +722 -0
- package/dist/editor-KZlceNQ1.d.ts.map +1 -0
- package/dist/{editor-DlGlYOp-.js → editor-TvRTsFdO.js} +102 -196
- package/dist/editor-TvRTsFdO.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 +5 -8
- package/dist/prosekit-core-test.js.map +1 -0
- package/dist/prosekit-core.d.ts +797 -792
- package/dist/prosekit-core.d.ts.map +1 -0
- package/dist/prosekit-core.js +42 -79
- package/dist/prosekit-core.js.map +1 -0
- package/package.json +14 -12
- 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 +125 -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.spec.ts +153 -0
- package/src/extensions/plugin.ts +81 -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.spec.ts +53 -0
- package/src/facets/state.ts +85 -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 +14 -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-OUH5V8BA.d.ts +0 -754
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Fragment,
|
|
3
|
+
Slice,
|
|
4
|
+
} from '@prosekit/pm/model'
|
|
5
|
+
import type {
|
|
6
|
+
Command,
|
|
7
|
+
Transaction,
|
|
8
|
+
} from '@prosekit/pm/state'
|
|
9
|
+
import { ReplaceAroundStep } from '@prosekit/pm/transform'
|
|
10
|
+
|
|
11
|
+
import type { CommandCreator } from '../types/extension-command'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export interface UnsetBlockTypeOptions {
|
|
17
|
+
/**
|
|
18
|
+
* The start position of the document. By default it will be the start position of current selection.
|
|
19
|
+
*/
|
|
20
|
+
from?: number
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The end position of the document. By default it will be the end position of current selection.
|
|
24
|
+
*/
|
|
25
|
+
to?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns a command that set the type of all textblocks between the given range
|
|
30
|
+
* to the default type (usually `paragraph`).
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
export function unsetBlockType(options?: UnsetBlockTypeOptions): Command {
|
|
35
|
+
return (state, dispatch) => {
|
|
36
|
+
const from = options?.from ?? state.selection.from
|
|
37
|
+
const to = options?.to ?? state.selection.to
|
|
38
|
+
if (from > to) return false
|
|
39
|
+
|
|
40
|
+
const tr = state.tr
|
|
41
|
+
if (unsetTextBlockType(tr, from, to)) {
|
|
42
|
+
dispatch?.(tr)
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function unsetTextBlockType(
|
|
50
|
+
tr: Transaction,
|
|
51
|
+
from: number,
|
|
52
|
+
to: number,
|
|
53
|
+
): boolean {
|
|
54
|
+
const mapFrom = tr.steps.length
|
|
55
|
+
tr.doc.nodesBetween(from, to, (node, pos, parent, index): boolean => {
|
|
56
|
+
if (!parent || !node.isTextblock) return true
|
|
57
|
+
|
|
58
|
+
const defaultType = parent.contentMatchAt(index).defaultType
|
|
59
|
+
if (
|
|
60
|
+
defaultType
|
|
61
|
+
&& defaultType.isTextblock
|
|
62
|
+
&& node.type !== defaultType
|
|
63
|
+
&& defaultType.validContent(node.content)
|
|
64
|
+
) {
|
|
65
|
+
const mapping = tr.mapping.slice(mapFrom)
|
|
66
|
+
const start = mapping.map(pos, 1)
|
|
67
|
+
const end = mapping.map(pos + node.nodeSize, 1)
|
|
68
|
+
const step = new ReplaceAroundStep(
|
|
69
|
+
start,
|
|
70
|
+
end,
|
|
71
|
+
start + 1,
|
|
72
|
+
end - 1,
|
|
73
|
+
new Slice(Fragment.from(defaultType.create()), 0, 0),
|
|
74
|
+
1,
|
|
75
|
+
true,
|
|
76
|
+
)
|
|
77
|
+
tr.step(step)
|
|
78
|
+
}
|
|
79
|
+
return false
|
|
80
|
+
})
|
|
81
|
+
return tr.steps.length > mapFrom
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
unsetBlockType satisfies CommandCreator
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
expect,
|
|
4
|
+
it,
|
|
5
|
+
} from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { setupTest } from '../testing'
|
|
8
|
+
|
|
9
|
+
describe('unsetMark', () => {
|
|
10
|
+
it('can unset marks', () => {
|
|
11
|
+
const { editor, m, n } = setupTest()
|
|
12
|
+
|
|
13
|
+
const doc1 = n.doc(
|
|
14
|
+
n.paragraph(
|
|
15
|
+
'<a>',
|
|
16
|
+
'1',
|
|
17
|
+
m.bold('2'),
|
|
18
|
+
m.italic('3'),
|
|
19
|
+
'<b>',
|
|
20
|
+
m.italic('4'),
|
|
21
|
+
m.italic('5'),
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const doc2 = n.doc(
|
|
26
|
+
n.paragraph('<a>', '123', '<b>', m.italic('4'), m.italic('5')),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
editor.set(doc1)
|
|
30
|
+
expect(editor.state.doc.toJSON()).toEqual(doc1.toJSON())
|
|
31
|
+
|
|
32
|
+
editor.commands.unsetMark()
|
|
33
|
+
expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Command } from '@prosekit/pm/state'
|
|
2
|
+
|
|
3
|
+
import type { CommandCreator } from '../types/extension-command'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @public
|
|
7
|
+
*
|
|
8
|
+
* Options for {@link unsetMark}.
|
|
9
|
+
*/
|
|
10
|
+
export interface UnsetMarkOptions {
|
|
11
|
+
/**
|
|
12
|
+
* The start position of the document. By default it will be the start position of current selection.
|
|
13
|
+
*/
|
|
14
|
+
from?: number
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The end position of the document. By default it will be the end position of current selection.
|
|
18
|
+
*/
|
|
19
|
+
to?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns a command that removes all marks.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
export function unsetMark(options?: UnsetMarkOptions): Command {
|
|
28
|
+
return (state, dispatch) => {
|
|
29
|
+
const from = options?.from ?? state.selection.from
|
|
30
|
+
const to = options?.to ?? state.selection.to
|
|
31
|
+
if (from > to) return false
|
|
32
|
+
|
|
33
|
+
dispatch?.(state.tr.removeMark(from, to))
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
unsetMark satisfies CommandCreator
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Attrs,
|
|
3
|
+
NodeType,
|
|
4
|
+
} from '@prosekit/pm/model'
|
|
5
|
+
import type { Command } from '@prosekit/pm/state'
|
|
6
|
+
import { findWrapping } from '@prosekit/pm/transform'
|
|
7
|
+
|
|
8
|
+
import { getNodeType } from '../utils/get-node-type'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export interface WrapOptions {
|
|
14
|
+
/**
|
|
15
|
+
* The node type to wrap the selected textblock with.
|
|
16
|
+
*/
|
|
17
|
+
type: NodeType | string
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @deprecated Use `nodeSpec` instead.
|
|
21
|
+
*/
|
|
22
|
+
nodeType?: NodeType
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Optional attributes to apply to the node.
|
|
26
|
+
*/
|
|
27
|
+
attrs?: Attrs | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns a command that wraps the selected textblock with the given node type.
|
|
32
|
+
*
|
|
33
|
+
* @param options
|
|
34
|
+
*
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
export function wrap(options: WrapOptions): Command {
|
|
38
|
+
return (state, dispatch) => {
|
|
39
|
+
const { $from, $to } = state.selection
|
|
40
|
+
const range = $from.blockRange($to)
|
|
41
|
+
if (!range) return false
|
|
42
|
+
|
|
43
|
+
const nodeType = getNodeType(state.schema, options.nodeType || options.type)
|
|
44
|
+
const wrapping = findWrapping(range, nodeType, options.attrs)
|
|
45
|
+
if (!wrapping) return false
|
|
46
|
+
|
|
47
|
+
dispatch?.(state.tr.wrap(range, wrapping))
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
expect,
|
|
4
|
+
it,
|
|
5
|
+
} from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { setupTest } from '../testing'
|
|
8
|
+
import { jsonFromNode } from '../utils/parse'
|
|
9
|
+
|
|
10
|
+
import type { NodeChild } from './action'
|
|
11
|
+
|
|
12
|
+
describe('NodeAction', () => {
|
|
13
|
+
const { editor, n } = setupTest()
|
|
14
|
+
|
|
15
|
+
it('can apply node', () => {
|
|
16
|
+
expect(n.heading('foo').toJSON()).toEqual({
|
|
17
|
+
type: 'heading',
|
|
18
|
+
content: [{ text: 'foo', type: 'text' }],
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('can apply node with attrs', () => {
|
|
23
|
+
expect(n.codeBlock({ language: 'javascript' }, 'foo').toJSON()).toEqual({
|
|
24
|
+
type: 'codeBlock',
|
|
25
|
+
attrs: { language: 'javascript' },
|
|
26
|
+
content: [{ text: 'foo', type: 'text' }],
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('can check node activity', () => {
|
|
31
|
+
editor.set(n.doc(n.heading('<a>foo<b>')))
|
|
32
|
+
expect(editor.nodes.heading.isActive()).toBe(true)
|
|
33
|
+
expect(editor.nodes.paragraph.isActive()).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('MarkAction', () => {
|
|
38
|
+
const { editor, m, n } = setupTest()
|
|
39
|
+
|
|
40
|
+
it('can apply mark', () => {
|
|
41
|
+
expect(n.p(m.bold('foo')).toJSON()).toEqual({
|
|
42
|
+
type: 'paragraph',
|
|
43
|
+
content: [{ marks: [{ type: 'bold' }], text: 'foo', type: 'text' }],
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('can apply mark with attrs', () => {
|
|
48
|
+
expect(
|
|
49
|
+
n.p(m.link({ href: 'https://example.com', target: '_blank', rel: 'noopener' }, 'foo')).toJSON(),
|
|
50
|
+
).toEqual({
|
|
51
|
+
type: 'paragraph',
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
marks: [{ type: 'link', attrs: { href: 'https://example.com', target: '_blank', rel: 'noopener' } }],
|
|
55
|
+
text: 'foo',
|
|
56
|
+
type: 'text',
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('can apply multiple marks', () => {
|
|
63
|
+
const json = jsonFromNode(n.p(m.bold(m.italic('foo'))))
|
|
64
|
+
const marks = json.content?.[0].marks?.map((mark) => mark.type)
|
|
65
|
+
expect(marks?.sort()).toEqual(['bold', 'italic'])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('can apply the same mark multiple times', () => {
|
|
69
|
+
const href1 = 'https://example.com/1'
|
|
70
|
+
const href2 = 'https://example.com/2'
|
|
71
|
+
const link1 = (node: NodeChild) => m.link({ href: href1 }, node)
|
|
72
|
+
const link2 = (node: NodeChild) => m.link({ href: href2, target: '_blank', rel: 'noopener' }, node)
|
|
73
|
+
|
|
74
|
+
expect(n.p(link1('foo')).toJSON()).toEqual({
|
|
75
|
+
type: 'paragraph',
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: 'text',
|
|
79
|
+
text: 'foo',
|
|
80
|
+
marks: [{ attrs: { href: href1, target: null, rel: null }, type: 'link' }],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(n.p(link2(link1('foo'))).toJSON()).toEqual({
|
|
86
|
+
type: 'paragraph',
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: 'text',
|
|
90
|
+
text: 'foo',
|
|
91
|
+
marks: [{ attrs: { href: href2, target: '_blank', rel: 'noopener' }, type: 'link' }],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
expect(n.p(link2(link2(link2('foo')))).toJSON()).toEqual({
|
|
97
|
+
type: 'paragraph',
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: 'text',
|
|
101
|
+
text: 'foo',
|
|
102
|
+
marks: [{ attrs: { href: href2, target: '_blank', rel: 'noopener' }, type: 'link' }],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('can check mark activity', () => {
|
|
109
|
+
editor.set(n.doc(n.p('<a>foo<b>')))
|
|
110
|
+
expect(editor.marks.bold.isActive()).toBe(false)
|
|
111
|
+
expect(editor.marks.italic.isActive()).toBe(false)
|
|
112
|
+
|
|
113
|
+
editor.set(n.doc(n.p('<a>', m.bold('foo'), '<b>')))
|
|
114
|
+
expect(editor.marks.bold.isActive()).toBe(true)
|
|
115
|
+
expect(editor.marks.italic.isActive()).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('can check mark activity for cross-paragraph selection', () => {
|
|
119
|
+
editor.set(n.doc(n.p('<a>', m.bold('foo')), n.p(m.bold('foo'), '<b>')))
|
|
120
|
+
|
|
121
|
+
expect(editor.marks.bold.isActive()).toBe(true)
|
|
122
|
+
expect(editor.marks.italic.isActive()).toBe(false)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should not set isActive to true when only part of the text is marked', () => {
|
|
126
|
+
editor.set(n.doc(n.p('<a>', 'foo', m.bold('bar'), 'baz', '<b>')))
|
|
127
|
+
expect(editor.marks.bold.isActive()).toBe(false)
|
|
128
|
+
|
|
129
|
+
editor.set(n.doc(n.p('<a>', m.bold('foo'), 'bar', '<b>')))
|
|
130
|
+
expect(editor.marks.bold.isActive()).toBe(false)
|
|
131
|
+
|
|
132
|
+
editor.set(n.doc(n.p('<a>', 'foo', m.bold('bar'), '<b>')))
|
|
133
|
+
expect(editor.marks.bold.isActive()).toBe(false)
|
|
134
|
+
|
|
135
|
+
editor.set(n.doc(n.p('<a>', m.bold('foo', 'bar'), '<b>')))
|
|
136
|
+
expect(editor.marks.bold.isActive()).toBe(true)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('should not set isActive to true when multiple empty paragraphs are selected', () => {
|
|
140
|
+
editor.set(n.doc(n.p('<a>'), n.p(''), n.p('<b>')))
|
|
141
|
+
expect(editor.marks.bold.isActive()).toBe(false)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Attrs,
|
|
3
|
+
Mark,
|
|
4
|
+
MarkType,
|
|
5
|
+
NodeType,
|
|
6
|
+
ProseMirrorNode,
|
|
7
|
+
Schema,
|
|
8
|
+
} from '@prosekit/pm/model'
|
|
9
|
+
import type { EditorState } from '@prosekit/pm/state'
|
|
10
|
+
import mapValues from 'just-map-values'
|
|
11
|
+
|
|
12
|
+
import { ProseKitError } from '../error'
|
|
13
|
+
import type { AnyAttrs } from '../types/attrs'
|
|
14
|
+
import { assert } from '../utils/assert'
|
|
15
|
+
import { isMarkActive } from '../utils/is-mark-active'
|
|
16
|
+
import { isNodeActive } from '../utils/is-node-active'
|
|
17
|
+
import { isProseMirrorNode } from '../utils/type-assertion'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Available children parameters for {@link NodeAction} and {@link MarkAction}.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
export type NodeChild = ProseMirrorNode | string | NodeChild[]
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A function for creating a node with optional attributes and any number of
|
|
28
|
+
* children.
|
|
29
|
+
*
|
|
30
|
+
* It also has a `isActive` method for checking if the node is active in the
|
|
31
|
+
* current editor selection.
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export interface NodeAction<Attrs extends AnyAttrs = AnyAttrs> {
|
|
36
|
+
/**
|
|
37
|
+
* Creates a node with attributes and any number of children.
|
|
38
|
+
*/
|
|
39
|
+
(attrs: Attrs | null, ...children: NodeChild[]): ProseMirrorNode
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates a node with any number of children.
|
|
43
|
+
*/
|
|
44
|
+
(...children: NodeChild[]): ProseMirrorNode
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Checks if the node is active in the current editor selection. If the
|
|
48
|
+
* optional `attrs` parameter is provided, it will check if the node is active
|
|
49
|
+
* with the given attributes.
|
|
50
|
+
*/
|
|
51
|
+
isActive: (attrs?: Attrs) => boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A function for applying a mark with optional attributes and any number of
|
|
56
|
+
* children.
|
|
57
|
+
*
|
|
58
|
+
* It also has a `isActive` method for checking if the mark is active in the
|
|
59
|
+
* current editor selection.
|
|
60
|
+
*
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
export interface MarkAction<Attrs extends AnyAttrs = AnyAttrs> {
|
|
64
|
+
/**
|
|
65
|
+
* Applies a mark with attributes and any number of children.
|
|
66
|
+
*/
|
|
67
|
+
(attrs: Attrs | null, ...children: NodeChild[]): ProseMirrorNode[]
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Applies a mark with any number of children.
|
|
71
|
+
*/
|
|
72
|
+
(...children: NodeChild[]): ProseMirrorNode[]
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Checks if the mark is active in the current editor selection. If the
|
|
76
|
+
* optional `attrs` parameter is provided, it will check if the mark is active
|
|
77
|
+
* with the given attributes.
|
|
78
|
+
*/
|
|
79
|
+
isActive: (attrs?: Attrs) => boolean
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @deprecated Use type {@link NodeAction} instead.
|
|
84
|
+
*/
|
|
85
|
+
export type NodeBuilder = NodeAction
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @deprecated Use type {@link MarkAction} instead.
|
|
89
|
+
*/
|
|
90
|
+
export type MarkBuilder = MarkAction
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @internal
|
|
94
|
+
*/
|
|
95
|
+
export function createNodeActions(
|
|
96
|
+
schema: Schema,
|
|
97
|
+
getState: GetStateFunction,
|
|
98
|
+
createNode: CreateNodeFunction = defaultCreateNode,
|
|
99
|
+
): Record<string, NodeAction> {
|
|
100
|
+
return mapValues(schema.nodes, (type) => createNodeAction(type, getState, createNode))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createNodeAction(
|
|
104
|
+
type: NodeType,
|
|
105
|
+
getState: GetStateFunction,
|
|
106
|
+
createNode: CreateNodeFunction,
|
|
107
|
+
): NodeAction {
|
|
108
|
+
const action = (
|
|
109
|
+
...args: [Attrs | NodeChild | null | undefined, ...NodeChild[]]
|
|
110
|
+
) => buildNode(type, args, createNode)
|
|
111
|
+
action.isActive = (attrs?: Attrs) => {
|
|
112
|
+
const state = getState()
|
|
113
|
+
return state ? isNodeActive(state, type, attrs) : false
|
|
114
|
+
}
|
|
115
|
+
return action
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
export function createMarkActions(
|
|
122
|
+
schema: Schema,
|
|
123
|
+
getState: GetStateFunction,
|
|
124
|
+
applyMark: ApplyMarkFunction = defaultApplyMark,
|
|
125
|
+
): Record<string, MarkAction> {
|
|
126
|
+
return mapValues(schema.marks, (type) => createMarkAction(type, getState, applyMark))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function createMarkAction(
|
|
130
|
+
type: MarkType,
|
|
131
|
+
getState: GetStateFunction,
|
|
132
|
+
applyMark: ApplyMarkFunction,
|
|
133
|
+
): MarkAction {
|
|
134
|
+
const action = (
|
|
135
|
+
...args: [Attrs | NodeChild | null | undefined, ...NodeChild[]]
|
|
136
|
+
) => buildMark(type, args, applyMark)
|
|
137
|
+
action.isActive = (attrs?: Attrs) => {
|
|
138
|
+
const state = getState()
|
|
139
|
+
return state ? isMarkActive(state, type, attrs) : false
|
|
140
|
+
}
|
|
141
|
+
return action
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildMark(
|
|
145
|
+
type: MarkType,
|
|
146
|
+
args: [Attrs | NodeChild | null | undefined, ...NodeChild[]],
|
|
147
|
+
applyMark: ApplyMarkFunction,
|
|
148
|
+
): ProseMirrorNode[] {
|
|
149
|
+
const [attrs, children] = normalizeArgs(args)
|
|
150
|
+
const mark = type.create(attrs)
|
|
151
|
+
return applyMark(mark, flattenChildren(type.schema, children))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @internal
|
|
156
|
+
*/
|
|
157
|
+
export type ApplyMarkFunction = (
|
|
158
|
+
mark: Mark,
|
|
159
|
+
children: ProseMirrorNode[],
|
|
160
|
+
) => ProseMirrorNode[]
|
|
161
|
+
|
|
162
|
+
const defaultApplyMark: ApplyMarkFunction = (
|
|
163
|
+
mark: Mark,
|
|
164
|
+
children: ProseMirrorNode[],
|
|
165
|
+
): ProseMirrorNode[] => {
|
|
166
|
+
return children.map((node) => node.mark(mark.addToSet(node.marks)))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildNode(
|
|
170
|
+
type: NodeType,
|
|
171
|
+
args: [Attrs | NodeChild | null | undefined, ...NodeChild[]],
|
|
172
|
+
createNode: CreateNodeFunction,
|
|
173
|
+
): ProseMirrorNode {
|
|
174
|
+
const [attrs, children] = normalizeArgs(args)
|
|
175
|
+
return createNode(type, attrs, flattenChildren(type.schema, children))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @internal
|
|
180
|
+
*/
|
|
181
|
+
export type CreateNodeFunction = (
|
|
182
|
+
type: NodeType,
|
|
183
|
+
attrs: Attrs | null,
|
|
184
|
+
children: ProseMirrorNode[],
|
|
185
|
+
) => ProseMirrorNode
|
|
186
|
+
|
|
187
|
+
type GetStateFunction = () => EditorState | null | undefined
|
|
188
|
+
|
|
189
|
+
const defaultCreateNode: CreateNodeFunction = (
|
|
190
|
+
type: NodeType,
|
|
191
|
+
attrs: Attrs | null,
|
|
192
|
+
children: ProseMirrorNode[],
|
|
193
|
+
) => {
|
|
194
|
+
const node = type.createAndFill(attrs, children)
|
|
195
|
+
assert(node, `Failed to create node ${type.name}`)
|
|
196
|
+
return node
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function flattenChildren(
|
|
200
|
+
schema: Schema,
|
|
201
|
+
children: NodeChild[],
|
|
202
|
+
): Array<ProseMirrorNode> {
|
|
203
|
+
const nodes: Array<ProseMirrorNode> = []
|
|
204
|
+
|
|
205
|
+
for (const child of children) {
|
|
206
|
+
if (typeof child === 'string') {
|
|
207
|
+
if (child) {
|
|
208
|
+
nodes.push(schema.text(child, null))
|
|
209
|
+
}
|
|
210
|
+
} else if (Array.isArray(child)) {
|
|
211
|
+
nodes.push(...flattenChildren(schema, child))
|
|
212
|
+
} else if (isProseMirrorNode(child)) {
|
|
213
|
+
nodes.push(child)
|
|
214
|
+
} else {
|
|
215
|
+
throw new ProseKitError(`Invalid node child: ${typeof child}`)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return nodes
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizeArgs(
|
|
223
|
+
args: [Attrs | NodeChild | null | undefined, ...NodeChild[]],
|
|
224
|
+
): [Attrs | null, NodeChild[]] {
|
|
225
|
+
const [attrs, ...children] = args
|
|
226
|
+
if (isNodeChild(attrs)) {
|
|
227
|
+
children.unshift(attrs)
|
|
228
|
+
return [null, children]
|
|
229
|
+
} else if (typeof attrs === 'object') {
|
|
230
|
+
return [attrs, children]
|
|
231
|
+
} else {
|
|
232
|
+
return [null, children]
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isNodeChild(
|
|
237
|
+
value: Attrs | NodeChild | null | undefined,
|
|
238
|
+
): value is NodeChild {
|
|
239
|
+
if (!value) {
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
typeof value === 'string'
|
|
245
|
+
|| Array.isArray(value)
|
|
246
|
+
|| isProseMirrorNode(value)
|
|
247
|
+
)
|
|
248
|
+
}
|