@prosekit/core 0.8.7 → 0.9.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.
Files changed (56) hide show
  1. package/dist/{editor-M9OimMiI.d.ts → editor-4lgGc3CY.d.ts} +20 -60
  2. package/dist/editor-4lgGc3CY.d.ts.map +1 -0
  3. package/dist/{editor-B0L9BgMi.js → editor-DGNUXn-u.js} +98 -115
  4. package/dist/editor-DGNUXn-u.js.map +1 -0
  5. package/dist/prosekit-core-test.d.ts +1 -1
  6. package/dist/prosekit-core-test.js +1 -1
  7. package/dist/prosekit-core.d.ts +81 -180
  8. package/dist/prosekit-core.d.ts.map +1 -1
  9. package/dist/prosekit-core.js +316 -403
  10. package/dist/prosekit-core.js.map +1 -1
  11. package/package.json +8 -10
  12. package/src/commands/select-all.ts +3 -8
  13. package/src/commands/select-block.spec.ts +83 -0
  14. package/src/commands/select-block.ts +59 -0
  15. package/src/commands/wrap.ts +1 -6
  16. package/src/editor/action.ts +1 -11
  17. package/src/editor/editor.ts +20 -32
  18. package/src/extensions/command.ts +4 -0
  19. package/src/extensions/default-state.spec.ts +0 -4
  20. package/src/extensions/default-state.ts +4 -24
  21. package/src/extensions/events/dom-event.ts +1 -4
  22. package/src/extensions/events/editor-event.ts +1 -1
  23. package/src/extensions/history.ts +1 -1
  24. package/src/extensions/keymap-base.spec.ts +98 -0
  25. package/src/extensions/keymap-base.ts +37 -13
  26. package/src/extensions/keymap.spec.ts +9 -5
  27. package/src/extensions/keymap.ts +13 -56
  28. package/src/extensions/mark-spec.ts +32 -29
  29. package/src/extensions/node-spec.ts +28 -19
  30. package/src/extensions/plugin.ts +1 -2
  31. package/src/facets/command.ts +8 -2
  32. package/src/facets/state.spec.ts +6 -6
  33. package/src/facets/state.ts +1 -2
  34. package/src/index.ts +3 -23
  35. package/src/types/extension-command.ts +0 -7
  36. package/src/types/extension.ts +0 -16
  37. package/src/utils/array-grouping.spec.ts +1 -11
  38. package/src/utils/array-grouping.ts +1 -14
  39. package/src/utils/array.ts +0 -4
  40. package/src/utils/combine-event-handlers.ts +4 -6
  41. package/src/utils/editor-content.ts +3 -3
  42. package/src/utils/output-spec.ts +11 -0
  43. package/src/utils/parse.ts +9 -9
  44. package/dist/editor-B0L9BgMi.js.map +0 -1
  45. package/dist/editor-M9OimMiI.d.ts.map +0 -1
  46. package/src/extensions/doc.ts +0 -31
  47. package/src/extensions/paragraph.ts +0 -61
  48. package/src/extensions/text.ts +0 -34
  49. package/src/types/base-node-view-options.ts +0 -33
  50. package/src/types/object-entries.ts +0 -13
  51. package/src/utils/collect-children.ts +0 -21
  52. package/src/utils/collect-nodes.ts +0 -37
  53. package/src/utils/deep-equals.spec.ts +0 -26
  54. package/src/utils/deep-equals.ts +0 -29
  55. package/src/utils/get-id.spec.ts +0 -14
  56. package/src/utils/get-id.ts +0 -13
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@prosekit/core",
3
3
  "type": "module",
4
- "version": "0.8.7",
4
+ "version": "0.9.0",
5
5
  "private": false,
6
6
  "description": "Core features for ProseKit",
7
7
  "author": {
@@ -40,24 +40,22 @@
40
40
  "src"
41
41
  ],
42
42
  "dependencies": {
43
- "@ocavue/utils": "^0.8.1",
43
+ "@ocavue/utils": "^1.2.0",
44
44
  "clsx": "^2.1.1",
45
- "just-clone": "^6.2.0",
46
- "just-map-values": "^3.2.0",
47
45
  "orderedmap": "^2.1.1",
48
46
  "prosemirror-splittable": "^0.1.1",
49
- "type-fest": "^5.2.0",
50
- "@prosekit/pm": "^0.1.14"
47
+ "type-fest": "^5.3.1",
48
+ "@prosekit/pm": "^0.1.15"
51
49
  },
52
50
  "devDependencies": {
53
51
  "@types/diffable-html": "^5.0.2",
54
52
  "diffable-html": "^6.0.1",
55
- "tsdown": "^0.16.5",
53
+ "tsdown": "^0.17.0",
56
54
  "typescript": "~5.9.3",
57
- "vitest": "^4.0.10",
55
+ "vitest": "^4.0.15",
58
56
  "vitest-browser-commands": "^0.2.0",
59
- "@prosekit/config-tsdown": "0.0.0",
60
- "@prosekit/config-vitest": "0.0.0"
57
+ "@prosekit/config-vitest": "0.0.0",
58
+ "@prosekit/config-tsdown": "0.0.0"
61
59
  },
62
60
  "publishConfig": {
63
61
  "dev": {}
@@ -1,7 +1,5 @@
1
- import {
2
- AllSelection,
3
- type Command,
4
- } from '@prosekit/pm/state'
1
+ import { selectAll as selectAllCommand } from '@prosekit/pm/commands'
2
+ import type { Command } from '@prosekit/pm/state'
5
3
 
6
4
  /**
7
5
  * Returns a command that selects the whole document.
@@ -9,8 +7,5 @@ import {
9
7
  * @public
10
8
  */
11
9
  export function selectAll(): Command {
12
- return (state, dispatch) => {
13
- dispatch?.(state.tr.setSelection(new AllSelection(state.doc)))
14
- return true
15
- }
10
+ return selectAllCommand
16
11
  }
@@ -0,0 +1,83 @@
1
+ import {
2
+ describe,
3
+ expect,
4
+ it,
5
+ } from 'vitest'
6
+
7
+ import { setupTest } from '../testing'
8
+
9
+ import { selectBlock } from './select-block'
10
+
11
+ describe('selectBlock', () => {
12
+ it('should expand the text selection to cover the start of the paragraph', () => {
13
+ const { editor, n, getSelectionString } = setup()
14
+ editor.set(n.doc(n.paragraph('Hello <a>world<b>')))
15
+ expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph("world")>"`)
16
+ editor.exec(selectBlock())
17
+ expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph("Hello world")>"`)
18
+ })
19
+
20
+ it('should expand the text selection to cover the end of the paragraph', () => {
21
+ const { editor, n, getSelectionString } = setup()
22
+ editor.set(n.doc(n.paragraph('<a>Hello<b> world')))
23
+ expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph("Hello")>"`)
24
+ editor.exec(selectBlock())
25
+ expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph("Hello world")>"`)
26
+ })
27
+
28
+ it('should expand the text selection to include other marks', () => {
29
+ const { editor, n, m, getSelectionString } = setup()
30
+ editor.set(n.doc(n.paragraph(
31
+ m.bold('Bold'),
32
+ ' ',
33
+ m.italic('Italic<a>'),
34
+ ' ',
35
+ m.bold('Bold'),
36
+ )))
37
+ expect(getSelectionString()).toMatchInlineSnapshot(`"<>"`)
38
+ editor.exec(selectBlock())
39
+ expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph(bold("Bold"), " ", italic("Italic"), " ", bold("Bold"))>"`)
40
+ })
41
+
42
+ it('should expand the text selection to include multiple blocks', () => {
43
+ const { editor, n, getSelectionString } = setup()
44
+ editor.set(n.doc(
45
+ n.paragraph('Hello<a>'),
46
+ n.paragraph('World'),
47
+ n.paragraph('<b>!'),
48
+ ))
49
+ expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph, paragraph("World"), paragraph>"`)
50
+ editor.exec(selectBlock())
51
+ expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph("Hello"), paragraph("World"), paragraph("!")>"`)
52
+ })
53
+
54
+ it('is able to expand the text selection pointing to different depths', () => {
55
+ const { editor, n, getSelectionString } = setup()
56
+ editor.set(n.doc(
57
+ n.paragraph('Hello<a>'),
58
+ n.blockquote([
59
+ n.paragraph('Item 1'),
60
+ n.blockquote([
61
+ n.paragraph('Sub<b> item 1.1'),
62
+ ]),
63
+ ]),
64
+ n.blockquote([
65
+ n.paragraph('Item 2'),
66
+ ]),
67
+ ))
68
+ expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph, blockquote(paragraph("Item 1"), blockquote(paragraph("Sub")))>"`)
69
+ editor.exec(selectBlock())
70
+ expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph("Hello"), blockquote(paragraph("Item 1"), blockquote(paragraph("Sub item 1.1")))>"`)
71
+ })
72
+ })
73
+
74
+ function setup() {
75
+ const { editor, n, m } = setupTest()
76
+
77
+ const getSelectionString = () => {
78
+ const fragment = editor.state.selection.content().content
79
+ return fragment.toString()
80
+ }
81
+
82
+ return { editor, n, m, getSelectionString }
83
+ }
@@ -0,0 +1,59 @@
1
+ import {
2
+ TextSelection,
3
+ type Command,
4
+ } from '@prosekit/pm/state'
5
+
6
+ import { isTextSelection } from '../utils/type-assertion'
7
+
8
+ // Based on https://github.com/ProseMirror/prosemirror-commands/blob/1.7.1/src/commands.ts#L507-L521
9
+ function getTextblockEndpoint(selection: TextSelection, side: number): number | undefined {
10
+ const $pos = side < 0 ? selection.$from : selection.$to
11
+ let depth = $pos.depth
12
+ while ($pos.node(depth).isInline) {
13
+ if (!depth) {
14
+ return
15
+ }
16
+ depth--
17
+ }
18
+ if (!$pos.node(depth).isTextblock) {
19
+ return
20
+ }
21
+ return side < 0 ? $pos.start(depth) : $pos.end(depth)
22
+ }
23
+
24
+ /**
25
+ * @internal
26
+ */
27
+ export const selectBlockCommand: Command = (state, dispatch) => {
28
+ const { selection } = state
29
+ if (!isTextSelection(selection)) {
30
+ return false
31
+ }
32
+
33
+ const expectedFrom = getTextblockEndpoint(selection, -1)
34
+ const expectedTo = getTextblockEndpoint(selection, 1)
35
+ if (expectedFrom == null || expectedTo == null) {
36
+ return false
37
+ }
38
+
39
+ if (selection.from <= expectedFrom && selection.to >= expectedTo) {
40
+ return false
41
+ }
42
+
43
+ if (dispatch) {
44
+ const newSelection = TextSelection.create(state.doc, expectedFrom, expectedTo)
45
+ dispatch(state.tr.setSelection(newSelection))
46
+ }
47
+ return true
48
+ }
49
+
50
+ /**
51
+ * Returns a command to expand the text selection to cover the current block
52
+ * node. If the text selection spans multiple blocks, it will select all
53
+ * blocks in the selection.
54
+ *
55
+ * @public
56
+ */
57
+ export function selectBlock(): Command {
58
+ return selectBlockCommand
59
+ }
@@ -16,11 +16,6 @@ export interface WrapOptions {
16
16
  */
17
17
  type: NodeType | string
18
18
 
19
- /**
20
- * @deprecated Use `nodeSpec` instead.
21
- */
22
- nodeType?: NodeType
23
-
24
19
  /**
25
20
  * Optional attributes to apply to the node.
26
21
  */
@@ -40,7 +35,7 @@ export function wrap(options: WrapOptions): Command {
40
35
  const range = $from.blockRange($to)
41
36
  if (!range) return false
42
37
 
43
- const nodeType = getNodeType(state.schema, options.nodeType || options.type)
38
+ const nodeType = getNodeType(state.schema, options.type)
44
39
  const wrapping = findWrapping(range, nodeType, options.attrs)
45
40
  if (!wrapping) return false
46
41
 
@@ -1,3 +1,4 @@
1
+ import { mapValues } from '@ocavue/utils'
1
2
  import type {
2
3
  Attrs,
3
4
  Mark,
@@ -7,7 +8,6 @@ import type {
7
8
  Schema,
8
9
  } from '@prosekit/pm/model'
9
10
  import type { EditorState } from '@prosekit/pm/state'
10
- import mapValues from 'just-map-values'
11
11
 
12
12
  import { ProseKitError } from '../error'
13
13
  import type { AnyAttrs } from '../types/attrs'
@@ -79,16 +79,6 @@ export interface MarkAction<Attrs extends AnyAttrs = AnyAttrs> {
79
79
  isActive: (attrs?: Attrs) => boolean
80
80
  }
81
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
82
  /**
93
83
  * @internal
94
84
  */
@@ -1,3 +1,4 @@
1
+ import { isDeepEqual } from '@ocavue/utils'
1
2
  import type {
2
3
  ProseMirrorNode,
3
4
  Schema,
@@ -40,7 +41,6 @@ import type {
40
41
  SelectionJSON,
41
42
  } from '../types/model'
42
43
  import { assert } from '../utils/assert'
43
- import { deepEquals } from '../utils/deep-equals'
44
44
  import {
45
45
  getEditorContentDoc,
46
46
  getEditorSelection,
@@ -70,25 +70,9 @@ export interface EditorOptions<E extends Extension> {
70
70
 
71
71
  /**
72
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.
73
+ * ProseMirror node JSON object, an HTML string, or a DOM element instance.
74
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
75
+ defaultContent?: NodeJSON | string | Element
92
76
 
93
77
  /**
94
78
  * A JSON object representing the starting selection to use when creating the
@@ -108,7 +92,7 @@ export interface getDocHTMLOptions extends DOMDocumentOptions {}
108
92
  export function setupEditorExtension<E extends Extension>(
109
93
  options: EditorOptions<E>,
110
94
  ): E {
111
- if (options.defaultContent || options.defaultDoc || options.defaultHTML) {
95
+ if (options.defaultContent) {
112
96
  return union(
113
97
  options.extension,
114
98
  defineDefaultState(options),
@@ -176,7 +160,7 @@ export class EditorInstance {
176
160
  return this.getState().doc
177
161
  }
178
162
 
179
- private getProp<PropName extends keyof EditorProps>(propName: PropName): EditorProps[PropName] | undefined {
163
+ private getProp<PropName extends keyof EditorProps>(propName: PropName): Partial<EditorProps>[PropName] {
180
164
  return this.view?.someProp(propName) ?? this.directEditorProps[propName]
181
165
  }
182
166
 
@@ -197,7 +181,7 @@ export class EditorInstance {
197
181
  }
198
182
 
199
183
  public setContent(
200
- content: NodeJSON | string | HTMLElement | ProseMirrorNode,
184
+ content: NodeJSON | string | Element | ProseMirrorNode,
201
185
  selection?: SelectionJSON | Selection | 'start' | 'end',
202
186
  ): void {
203
187
  const doc = getEditorContentDoc(this.schema, content)
@@ -226,7 +210,7 @@ export class EditorInstance {
226
210
  }
227
211
 
228
212
  /**
229
- * Return a HTML string representing the editor's current document.
213
+ * Return an HTML string representing the editor's current document.
230
214
  */
231
215
  public getDocHTML = (options?: getDocHTMLOptions): string => {
232
216
  const serializer = this.getProp('clipboardSerializer')
@@ -264,14 +248,14 @@ export class EditorInstance {
264
248
  const newPayload = this.tree.getRootOutput()
265
249
  const newPlugins = [...(newPayload?.state?.plugins ?? [])]
266
250
 
267
- if (!deepEquals(oldPlugins, newPlugins)) {
251
+ if (!isDeepEqual(oldPlugins, newPlugins)) {
268
252
  const state = view.state.reconfigure({ plugins: newPlugins })
269
253
  view.updateState(state)
270
254
  }
271
255
 
272
256
  if (
273
257
  newPayload?.commands
274
- && !deepEquals(oldPayload?.commands, newPayload?.commands)
258
+ && !isDeepEqual(oldPayload?.commands, newPayload?.commands)
275
259
  ) {
276
260
  const commands = newPayload.commands
277
261
  const names = Object.keys(commands)
@@ -306,6 +290,9 @@ export class EditorInstance {
306
290
 
307
291
  public mount(place: HTMLElement): void {
308
292
  if (this.view) {
293
+ // If the editor is already mounted to the same DOM element, do nothing
294
+ if (this.view.dom === place) return
295
+ // If the editor is already mounted to a different element, throw an error
309
296
  throw new ProseKitError('Editor is already mounted')
310
297
  }
311
298
  this.view = new EditorView({ mount: place }, this.directEditorProps)
@@ -374,7 +361,6 @@ export class EditorInstance {
374
361
  return this.canExec(command)
375
362
  }
376
363
 
377
- action.canApply = canExec
378
364
  action.canExec = canExec
379
365
 
380
366
  this.commands[name] = action as CommandAction
@@ -437,12 +423,14 @@ export class Editor<E extends Extension = any> {
437
423
  }
438
424
 
439
425
  /**
440
- * Mount the editor to the given HTML element.
441
- * Pass `null` or `undefined` to unmount the editor.
426
+ * Mount the editor to the given HTML element. Pass `null` or `undefined` to
427
+ * unmount the editor. When an element is passed, this method returns a
428
+ * function to unmount the editor.
442
429
  */
443
- mount = (place: HTMLElement | null | undefined): void => {
430
+ mount = (place: HTMLElement | null | undefined): void | VoidFunction => {
444
431
  if (place) {
445
432
  this.instance.mount(place)
433
+ return this.unmount
446
434
  } else {
447
435
  this.instance.unmount()
448
436
  }
@@ -496,7 +484,7 @@ export class Editor<E extends Extension = any> {
496
484
  * - A ProseMirror node instance
497
485
  * - A ProseMirror node JSON object
498
486
  * - An HTML string
499
- * - An HTML element instance
487
+ * - A DOM element instance
500
488
  * @param selection - Optional. Specifies the new selection. It can be one of the following:
501
489
  * - A ProseMirror selection instance
502
490
  * - A ProseMirror selection JSON object
@@ -504,7 +492,7 @@ export class Editor<E extends Extension = any> {
504
492
  * - The string "end" (to set selection at the end)
505
493
  */
506
494
  setContent = (
507
- content: ProseMirrorNode | NodeJSON | string | HTMLElement,
495
+ content: ProseMirrorNode | NodeJSON | string | Element,
508
496
  selection?: SelectionJSON | Selection | 'start' | 'end',
509
497
  ): void => {
510
498
  return this.instance.setContent(content, selection)
@@ -518,7 +506,7 @@ export class Editor<E extends Extension = any> {
518
506
  }
519
507
 
520
508
  /**
521
- * Return a HTML string representing the editor's current document.
509
+ * Return an HTML string representing the editor's current document.
522
510
  */
523
511
  public getDocHTML = (options?: getDocHTMLOptions): string => {
524
512
  return this.instance.getDocHTML(options)
@@ -23,6 +23,7 @@ import {
23
23
  type RemoveNodeOptions,
24
24
  } from '../commands/remove-node'
25
25
  import { selectAll } from '../commands/select-all'
26
+ import { selectBlock } from '../commands/select-block'
26
27
  import {
27
28
  setBlockType,
28
29
  type SetBlockTypeOptions,
@@ -78,6 +79,7 @@ export type BaseCommandsExtension = Extension<{
78
79
  setNodeAttrs: [options: SetNodeAttrsOptions]
79
80
  insertDefaultBlock: [options?: InsertDefaultBlockOptions]
80
81
  selectAll: []
82
+ selectBlock: []
81
83
  addMark: [options: AddMarkOptions]
82
84
  removeMark: [options: RemoveMarkOptions]
83
85
  unsetBlockType: [options?: UnsetBlockTypeOptions]
@@ -110,6 +112,8 @@ export function defineBaseCommands(): BaseCommandsExtension {
110
112
 
111
113
  selectAll,
112
114
 
115
+ selectBlock,
116
+
113
117
  addMark,
114
118
 
115
119
  removeMark,
@@ -27,10 +27,6 @@ describe('defineDefaultState', () => {
27
27
  return editor.state.doc.toString()
28
28
  }
29
29
 
30
- expect(run({ defaultDoc: docJSON })).toContain('docJSON')
31
- expect(run({ defaultHTML: docHTMLString })).toContain('docHTMLString')
32
- expect(run({ defaultHTML: docHTMLElement })).toContain('docHTMLElement')
33
-
34
30
  expect(run({ defaultContent: docJSON })).toContain('docJSON')
35
31
  expect(run({ defaultContent: docHTMLString })).toContain('docHTMLString')
36
32
  expect(run({ defaultContent: docHTMLElement })).toContain('docHTMLElement')
@@ -18,25 +18,9 @@ import { getEditorContentJSON } from '../utils/editor-content'
18
18
  export interface DefaultStateOptions {
19
19
  /**
20
20
  * The starting document to use when creating the editor. It can be a
21
- * ProseMirror node JSON object, a HTML string, or a HTML element instance.
21
+ * ProseMirror node JSON object, an HTML string, or a DOM element instance.
22
22
  */
23
- defaultContent?: NodeJSON | string | HTMLElement
24
-
25
- /**
26
- * A JSON object representing the starting document to use when creating the
27
- * editor.
28
- *
29
- * @deprecated Use `defaultContent` instead.
30
- */
31
- defaultDoc?: NodeJSON
32
-
33
- /**
34
- * A HTML element or a HTML string representing the starting document to use
35
- * when creating the editor.
36
- *
37
- * @deprecated Use `defaultContent` instead.
38
- */
39
- defaultHTML?: string | HTMLElement
23
+ defaultContent?: NodeJSON | string | Element
40
24
 
41
25
  /**
42
26
  * A JSON object representing the starting selection to use when creating the
@@ -55,16 +39,12 @@ export interface DefaultStateOptions {
55
39
  export function defineDefaultState({
56
40
  defaultSelection,
57
41
  defaultContent,
58
- defaultDoc,
59
- defaultHTML,
60
42
  }: DefaultStateOptions): PlainExtension {
61
- const defaultDocContent = defaultContent || defaultDoc || defaultHTML
62
-
63
43
  return defineFacetPayload(stateFacet, [
64
44
  ({ schema }) => {
65
45
  const config: EditorStateConfig = {}
66
- if (defaultDocContent) {
67
- const json = getEditorContentJSON(schema, defaultDocContent)
46
+ if (defaultContent) {
47
+ const json = getEditorContentJSON(schema, defaultContent)
68
48
  config.doc = schema.nodeFromJSON(json)
69
49
  if (defaultSelection) {
70
50
  config.selection = Selection.fromJSON(config.doc, defaultSelection)
@@ -85,10 +85,7 @@ const domEventFacet: Facet<DOMEventPayload, PluginPayload> = defineFacet(
85
85
  hasNewEvent = true
86
86
  const [setHandlers, combinedHandler] = combineEventHandlers<DOMEventHandler>()
87
87
  setHandlersMap[event] = setHandlers
88
- const e: DOMEventHandler = (view, eventObject) => {
89
- return combinedHandler(view, eventObject)
90
- }
91
- combinedHandlerMap[event] = e
88
+ combinedHandlerMap[event] = combinedHandler
92
89
  }
93
90
  }
94
91
 
@@ -1,3 +1,4 @@
1
+ import type { ObjectEntries } from '@ocavue/utils'
1
2
  import type {
2
3
  Node,
3
4
  Slice,
@@ -14,7 +15,6 @@ import {
14
15
  } from '../../facets/facet'
15
16
  import { defineFacetPayload } from '../../facets/facet-extension'
16
17
  import type { PlainExtension } from '../../types/extension'
17
- import type { ObjectEntries } from '../../types/object-entries'
18
18
  import { groupEntries } from '../../utils/array-grouping'
19
19
  import { combineEventHandlers } from '../../utils/combine-event-handlers'
20
20
  import {
@@ -17,7 +17,7 @@ import { definePlugin } from './plugin'
17
17
 
18
18
  const keymap: Keymap = {
19
19
  'Mod-z': undo,
20
- 'Shift-Mod-z': redo,
20
+ 'Mod-Z': redo,
21
21
  }
22
22
 
23
23
  if (!isApple) {
@@ -0,0 +1,98 @@
1
+ import {
2
+ describe,
3
+ expect,
4
+ it,
5
+ } from 'vitest'
6
+ import { keyboard } from 'vitest-browser-commands/playwright'
7
+
8
+ import { union } from '../editor/union'
9
+ import type { TestEditor } from '../test'
10
+ import {
11
+ defineDoc,
12
+ defineParagraph,
13
+ defineText,
14
+ setupTestFromExtension,
15
+ } from '../testing'
16
+ import type { SelectionJSON } from '../types/model'
17
+
18
+ import { defineBaseKeymap } from './keymap-base'
19
+
20
+ describe('Mod-a', () => {
21
+ it('can select the block for the first Mod-a press', async () => {
22
+ const { editor, n } = setupTestFromExtension(union(
23
+ defineDoc(),
24
+ defineText(),
25
+ defineParagraph(),
26
+ defineBaseKeymap(),
27
+ ))
28
+
29
+ editor.set(n.doc(n.paragraph('Fo<a>o<b>'), n.paragraph('Bar')))
30
+ await keyboard.press('ControlOrMeta+a')
31
+ expect(inspectSelection(editor)).toMatchInlineSnapshot(`"text: <paragraph("Foo")>"`)
32
+ await keyboard.press('ControlOrMeta+a')
33
+ expect(inspectSelection(editor)).toMatchInlineSnapshot(`"all: <paragraph("Foo"), paragraph("Bar")>"`)
34
+ })
35
+
36
+ it('can select the entire document if the current textblock is already selected', async () => {
37
+ const { editor, n } = setupTestFromExtension(union(
38
+ defineDoc(),
39
+ defineText(),
40
+ defineParagraph(),
41
+ defineBaseKeymap(),
42
+ ))
43
+
44
+ editor.set(n.doc(n.paragraph('<a>Foo<b>'), n.paragraph('Bar')))
45
+ expect(inspectSelection(editor)).toMatchInlineSnapshot(`"text: <paragraph("Foo")>"`)
46
+ await keyboard.press('ControlOrMeta+a')
47
+ expect(inspectSelection(editor)).toMatchInlineSnapshot(`"all: <paragraph("Foo"), paragraph("Bar")>"`)
48
+ })
49
+
50
+ it('can select the entire document if multiple textblocks are already selected', async () => {
51
+ const { editor, n } = setupTestFromExtension(union(
52
+ defineDoc(),
53
+ defineText(),
54
+ defineParagraph(),
55
+ defineBaseKeymap(),
56
+ ))
57
+
58
+ editor.set(n.doc(n.paragraph('<a>Foo'), n.paragraph('Bar<b>'), n.paragraph('Baz')))
59
+ expect(inspectSelection(editor)).toMatchInlineSnapshot(`"text: <paragraph("Foo"), paragraph("Bar")>"`)
60
+ await keyboard.press('ControlOrMeta+a')
61
+ expect(inspectSelection(editor)).toMatchInlineSnapshot(`"all: <paragraph("Foo"), paragraph("Bar"), paragraph("Baz")>"`)
62
+ })
63
+
64
+ it('can select the entire document if the current textblock is empty', async () => {
65
+ const { editor, n } = setupTestFromExtension(union(
66
+ defineDoc(),
67
+ defineText(),
68
+ defineParagraph(),
69
+ defineBaseKeymap(),
70
+ ))
71
+
72
+ editor.set(n.doc(n.paragraph('Foo'), n.paragraph('<a>'), n.paragraph('Bar')))
73
+ expect(inspectSelection(editor)).toMatchInlineSnapshot(`"text: <>"`)
74
+ await keyboard.press('ControlOrMeta+a')
75
+ expect(inspectSelection(editor)).toMatchInlineSnapshot(`"all: <paragraph("Foo"), paragraph, paragraph("Bar")>"`)
76
+ })
77
+
78
+ it('can select the entire document directly if `preferBlockSelection` is false', async () => {
79
+ const { editor, n } = setupTestFromExtension(union(
80
+ defineDoc(),
81
+ defineText(),
82
+ defineParagraph(),
83
+ defineBaseKeymap({ preferBlockSelection: false }),
84
+ ))
85
+
86
+ editor.set(n.doc(n.paragraph('<a>Foo<b>'), n.paragraph('Bar')))
87
+ expect(inspectSelection(editor)).toMatchInlineSnapshot(`"text: <paragraph("Foo")>"`)
88
+ await keyboard.press('ControlOrMeta+a')
89
+ expect(inspectSelection(editor)).toMatchInlineSnapshot(`"all: <paragraph("Foo"), paragraph("Bar")>"`)
90
+ })
91
+ })
92
+
93
+ function inspectSelection(editor: TestEditor) {
94
+ const selection = editor.state.selection
95
+ const text = selection.content().content.toString()
96
+ const json = selection.toJSON() as SelectionJSON
97
+ return `${json.type}: ${text}`
98
+ }