@blocknote/core 0.1.1 → 0.2.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 (171) hide show
  1. package/README.md +12 -6
  2. package/dist/blocknote.js +1425 -5114
  3. package/dist/blocknote.js.map +1 -1
  4. package/dist/blocknote.umd.cjs +1 -53
  5. package/dist/blocknote.umd.cjs.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +3 -16
  8. package/src/BlockNoteEditor.ts +54 -0
  9. package/src/BlockNoteExtensions.ts +52 -7
  10. package/src/assets/fonts-inter.css +92 -0
  11. package/src/editor.module.css +37 -0
  12. package/src/extensions/Blocks/BlockAttributes.ts +1 -3
  13. package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +71 -18
  14. package/src/extensions/Blocks/helpers/getBlockInfoFromPos.ts +66 -0
  15. package/src/extensions/Blocks/index.ts +7 -3
  16. package/src/extensions/Blocks/nodes/Block.module.css +116 -74
  17. package/src/extensions/Blocks/nodes/Block.ts +415 -292
  18. package/src/extensions/Blocks/nodes/BlockGroup.ts +6 -6
  19. package/src/extensions/Blocks/nodes/BlockTypes/HeadingBlock/HeadingContent.ts +84 -0
  20. package/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/ListItemContent.ts +177 -0
  21. package/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/OrderedListItemIndexPlugin.ts +77 -0
  22. package/src/extensions/Blocks/nodes/BlockTypes/TextBlock/TextContent.ts +34 -0
  23. package/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts +20 -0
  24. package/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +27 -9
  25. package/src/extensions/DraggableBlocks/{DraggableBlocksPlugin.tsx → DraggableBlocksPlugin.ts} +227 -147
  26. package/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts +29 -0
  27. package/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +35 -0
  28. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +308 -0
  29. package/src/extensions/HyperlinkToolbar/HyperlinkMark.ts +28 -0
  30. package/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts +19 -0
  31. package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +251 -0
  32. package/src/extensions/Placeholder/PlaceholderExtension.ts +2 -2
  33. package/src/extensions/SlashMenu/SlashMenuExtension.ts +9 -1
  34. package/src/extensions/SlashMenu/SlashMenuItem.ts +1 -3
  35. package/src/extensions/SlashMenu/defaultCommands.tsx +33 -22
  36. package/src/extensions/TrailingNode/TrailingNodeExtension.ts +4 -4
  37. package/src/extensions/UniqueID/UniqueID.ts +14 -12
  38. package/src/index.ts +8 -4
  39. package/src/shared/EditorElement.ts +10 -0
  40. package/src/shared/plugins/suggestion/SuggestionItem.ts +1 -8
  41. package/src/shared/plugins/suggestion/SuggestionPlugin.ts +222 -101
  42. package/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts +21 -0
  43. package/src/{utils.ts → shared/utils.ts} +0 -0
  44. package/types/src/BlockNoteEditor.d.ts +13 -0
  45. package/types/src/BlockNoteExtensions.d.ts +12 -1
  46. package/types/src/EditorElement.d.ts +7 -0
  47. package/types/src/extensions/Blocks/PreviousBlockTypePlugin.d.ts +1 -1
  48. package/types/src/extensions/Blocks/helpers/getBlockInfoFromPos.d.ts +19 -0
  49. package/types/src/extensions/Blocks/nodes/Block.d.ts +11 -19
  50. package/types/src/extensions/Blocks/nodes/BlockTypes/HeadingBlock/HeadingContent.d.ts +8 -0
  51. package/types/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/ListItemContent.d.ts +8 -0
  52. package/types/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/OrderedListItemIndexPlugin.d.ts +2 -0
  53. package/types/src/extensions/Blocks/nodes/BlockTypes/TextBlock/TextContent.d.ts +6 -0
  54. package/types/src/extensions/BubbleMenu/BubbleMenuExtension.d.ts +4 -1
  55. package/types/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.d.ts +27 -0
  56. package/types/src/extensions/BubbleMenu/BubbleMenuPlugin.d.ts +10 -12
  57. package/types/src/extensions/DraggableBlocks/BlockMenuFactoryTypes.d.ts +12 -0
  58. package/types/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.d.ts +14 -0
  59. package/types/src/extensions/DraggableBlocks/DragMenuFactoryTypes.d.ts +18 -0
  60. package/types/src/extensions/DraggableBlocks/DraggableBlocksExtension.d.ts +9 -3
  61. package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +23 -1
  62. package/types/src/extensions/FormattingToolbar/FormattingToolbarExtension.d.ts +8 -0
  63. package/types/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.d.ts +23 -0
  64. package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +43 -0
  65. package/types/src/extensions/HyperlinkToolbar/HyperlinkMark.d.ts +8 -0
  66. package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.d.ts +12 -0
  67. package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.d.ts +11 -0
  68. package/types/src/extensions/Hyperlinks/HyperlinkMark.d.ts +2 -1
  69. package/types/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.d.ts +11 -0
  70. package/types/src/extensions/Hyperlinks/HyperlinkMenuPlugin.d.ts +10 -1
  71. package/types/src/extensions/SlashMenu/SlashMenuExtension.d.ts +3 -1
  72. package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +2 -4
  73. package/types/src/index.d.ts +8 -3
  74. package/types/src/shared/EditorElement.d.ts +6 -0
  75. package/types/src/shared/plugins/suggestion/SuggestionItem.d.ts +1 -6
  76. package/types/src/shared/plugins/suggestion/SuggestionPlugin.d.ts +15 -10
  77. package/types/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.d.ts +12 -0
  78. package/types/src/shared/utils.d.ts +2 -0
  79. package/types/src/utils.d.ts +2 -2
  80. package/src/BlockNoteTheme.ts +0 -150
  81. package/src/EditorContent.tsx +0 -2
  82. package/src/extensions/Blocks/OrderedListPlugin.ts +0 -46
  83. package/src/extensions/Blocks/commands/joinBackward.ts +0 -274
  84. package/src/extensions/Blocks/helpers/setBlockHeading.ts +0 -30
  85. package/src/extensions/Blocks/nodes/Content.ts +0 -63
  86. package/src/extensions/Blocks/rule.ts +0 -48
  87. package/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +0 -36
  88. package/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +0 -245
  89. package/src/extensions/BubbleMenu/component/BubbleMenu.tsx +0 -240
  90. package/src/extensions/BubbleMenu/component/LinkToolbarButton.tsx +0 -67
  91. package/src/extensions/DraggableBlocks/components/DragHandle.tsx +0 -102
  92. package/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx +0 -19
  93. package/src/extensions/Hyperlinks/HyperlinkMark.tsx +0 -16
  94. package/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +0 -165
  95. package/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.tsx +0 -44
  96. package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItem.tsx +0 -34
  97. package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemIcon.tsx +0 -31
  98. package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemInput.tsx +0 -40
  99. package/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.tsx +0 -37
  100. package/src/extensions/Hyperlinks/menus/HyperlinkMenu.tsx +0 -63
  101. package/src/fonts-inter.css +0 -94
  102. package/src/globals.css +0 -28
  103. package/src/root.module.css +0 -19
  104. package/src/shared/components/toolbar/Toolbar.tsx +0 -10
  105. package/src/shared/components/toolbar/ToolbarButton.tsx +0 -57
  106. package/src/shared/components/toolbar/ToolbarDropdown.tsx +0 -35
  107. package/src/shared/components/toolbar/ToolbarDropdownItem.tsx +0 -35
  108. package/src/shared/components/toolbar/ToolbarDropdownTarget.tsx +0 -31
  109. package/src/shared/components/tooltip/TooltipContent.module.css +0 -15
  110. package/src/shared/components/tooltip/TooltipContent.tsx +0 -23
  111. package/src/shared/hooks/useEditorForceUpdate.tsx +0 -30
  112. package/src/shared/plugins/suggestion/SuggestionListReactRenderer.tsx +0 -236
  113. package/src/shared/plugins/suggestion/components/SuggestionGroup.tsx +0 -47
  114. package/src/shared/plugins/suggestion/components/SuggestionGroupItem.tsx +0 -82
  115. package/src/shared/plugins/suggestion/components/SuggestionList.tsx +0 -92
  116. package/src/useEditor.ts +0 -51
  117. package/types/src/BlockNoteTheme.d.ts +0 -2
  118. package/types/src/EditorContent.d.ts +0 -1
  119. package/types/src/commands/indentation.d.ts +0 -2
  120. package/types/src/extensions/Blocks/OrderedListPlugin.d.ts +0 -2
  121. package/types/src/extensions/Blocks/commands/joinBackward.d.ts +0 -14
  122. package/types/src/extensions/Blocks/helpers/setBlockHeading.d.ts +0 -5
  123. package/types/src/extensions/Blocks/nodes/Content.d.ts +0 -5
  124. package/types/src/extensions/Blocks/rule.d.ts +0 -16
  125. package/types/src/extensions/BubbleMenu/component/BubbleMenu.d.ts +0 -5
  126. package/types/src/extensions/BubbleMenu/component/DropdownBlockItem.d.ts +0 -10
  127. package/types/src/extensions/BubbleMenu/component/LinkToolbarButton.d.ts +0 -11
  128. package/types/src/extensions/DraggableBlocks/components/DragHandle.d.ts +0 -12
  129. package/types/src/extensions/DraggableBlocks/components/DragHandleMenu.d.ts +0 -6
  130. package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.d.ts +0 -11
  131. package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItem.d.ts +0 -13
  132. package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemIcon.d.ts +0 -8
  133. package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemInput.d.ts +0 -9
  134. package/types/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.d.ts +0 -12
  135. package/types/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.d.ts +0 -12
  136. package/types/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.d.ts +0 -10
  137. package/types/src/extensions/Hyperlinks/menus/HyperlinkMenu.d.ts +0 -21
  138. package/types/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.d.ts +0 -39
  139. package/types/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.d.ts +0 -1
  140. package/types/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.d.ts +0 -11
  141. package/types/src/extensions/Hyperlinks/menus/helpers/PanelTextInput.d.ts +0 -39
  142. package/types/src/extensions/Hyperlinks/menus/helpers/PanelTextInputStyles.d.ts +0 -3
  143. package/types/src/extensions/Hyperlinks/menus/helpers/ToolbarComponent.d.ts +0 -13
  144. package/types/src/extensions/helpers/formatKeyboardShortcut.d.ts +0 -1
  145. package/types/src/lib/atlaskit/browser.d.ts +0 -12
  146. package/types/src/nodes/ChildgroupNode.d.ts +0 -28
  147. package/types/src/nodes/patchNodes.d.ts +0 -1
  148. package/types/src/plugins/TreeViewPlugin/index.d.ts +0 -2
  149. package/types/src/plugins/animation.d.ts +0 -2
  150. package/types/src/react/BlockNoteComposer.d.ts +0 -17
  151. package/types/src/react/BlockNotePlugin.d.ts +0 -1
  152. package/types/src/react/index.d.ts +0 -3
  153. package/types/src/react/useBlockNoteSetup.d.ts +0 -2
  154. package/types/src/registerBlockNote.d.ts +0 -2
  155. package/types/src/shared/components/toolbar/SimpleToolbarButton.d.ts +0 -15
  156. package/types/src/shared/components/toolbar/SimpleToolbarDropdown.d.ts +0 -11
  157. package/types/src/shared/components/toolbar/SimpleToolbarDropdownItem.d.ts +0 -11
  158. package/types/src/shared/components/toolbar/Toolbar.d.ts +0 -4
  159. package/types/src/shared/components/toolbar/ToolbarButton.d.ts +0 -15
  160. package/types/src/shared/components/toolbar/ToolbarDropdown.d.ts +0 -17
  161. package/types/src/shared/components/toolbar/ToolbarDropdownItem.d.ts +0 -11
  162. package/types/src/shared/components/toolbar/ToolbarDropdownTarget.d.ts +0 -8
  163. package/types/src/shared/components/toolbar/ToolbarSeparator.d.ts +0 -2
  164. package/types/src/shared/components/tooltip/TooltipContent.d.ts +0 -15
  165. package/types/src/shared/hooks/useEditorForceUpdate.d.ts +0 -2
  166. package/types/src/shared/plugins/suggestion/SuggestionListReactRenderer.d.ts +0 -71
  167. package/types/src/shared/plugins/suggestion/components/SuggestionGroup.d.ts +0 -23
  168. package/types/src/shared/plugins/suggestion/components/SuggestionGroupItem.d.ts +0 -9
  169. package/types/src/shared/plugins/suggestion/components/SuggestionList.d.ts +0 -11
  170. package/types/src/themes/BlockNoteEditorTheme.d.ts +0 -11
  171. package/types/src/useEditor.d.ts +0 -11
@@ -1,41 +1,38 @@
1
1
  import { mergeAttributes, Node } from "@tiptap/core";
2
- import { Selection, TextSelection } from "prosemirror-state";
3
- import { joinBackward } from "../commands/joinBackward";
4
- import { findBlock } from "../helpers/findBlock";
5
- import { setBlockHeading } from "../helpers/setBlockHeading";
6
- import { OrderedListPlugin } from "../OrderedListPlugin";
2
+ import { Slice } from "prosemirror-model";
3
+ import { TextSelection } from "prosemirror-state";
4
+ import BlockAttributes from "../BlockAttributes";
5
+ import { getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos";
7
6
  import { PreviousBlockTypePlugin } from "../PreviousBlockTypePlugin";
8
- import { textblockTypeInputRuleSameNodeType } from "../rule";
9
7
  import styles from "./Block.module.css";
10
- import BlockAttributes from "../BlockAttributes";
8
+ import { TextContentType } from "./BlockTypes/TextBlock/TextContent";
9
+ import { HeadingContentType } from "./BlockTypes/HeadingBlock/HeadingContent";
10
+ import { ListItemContentType } from "./BlockTypes/ListItemBlock/ListItemContent";
11
11
 
12
12
  export interface IBlock {
13
13
  HTMLAttributes: Record<string, any>;
14
14
  }
15
15
 
16
- export type Level = "1" | "2" | "3";
17
- export type ListType = "li" | "oli";
16
+ export type BlockContentType =
17
+ | TextContentType
18
+ | HeadingContentType
19
+ | ListItemContentType;
18
20
 
19
21
  declare module "@tiptap/core" {
20
22
  interface Commands<ReturnType> {
21
- blockHeading: {
22
- /**
23
- * Set a heading node
24
- */
25
- setBlockHeading: (attributes: { level: Level }) => ReturnType;
26
- /**
27
- * Unset a heading node
28
- */
29
- unsetBlockHeading: () => ReturnType;
30
-
31
- unsetList: () => ReturnType;
32
-
33
- addNewBlockAsSibling: (attributes?: {
34
- headingType?: Level;
35
- listType?: ListType;
36
- }) => ReturnType;
37
-
38
- setBlockList: (type: ListType) => ReturnType;
23
+ block: {
24
+ BNCreateBlock: (pos: number) => ReturnType;
25
+ BNDeleteBlock: (posInBlock: number) => ReturnType;
26
+ BNMergeBlocks: (posBetweenBlocks: number) => ReturnType;
27
+ BNSplitBlock: (posInBlock: number, keepType: boolean) => ReturnType;
28
+ BNSetContentType: (
29
+ posInBlock: number,
30
+ type: BlockContentType
31
+ ) => ReturnType;
32
+ BNCreateBlockOrSetContentType: (
33
+ posInBlock: number,
34
+ type: BlockContentType
35
+ ) => ReturnType;
39
36
  };
40
37
  }
41
38
  }
@@ -46,38 +43,31 @@ declare module "@tiptap/core" {
46
43
  export const Block = Node.create<IBlock>({
47
44
  name: "block",
48
45
  group: "block",
46
+ // A block always contains content, and optionally a blockGroup which contains nested blocks
47
+ content: "blockContent blockGroup?",
48
+ // Ensures content-specific keyboard handlers trigger first.
49
+ priority: 50,
50
+ defining: true,
51
+
49
52
  addOptions() {
50
53
  return {
51
54
  HTMLAttributes: {},
52
55
  };
53
56
  },
54
57
 
55
- // A block always contains content, and optionally a blockGroup which contains nested blocks
56
- content: "content blockgroup?",
57
-
58
- defining: true,
59
-
60
58
  addAttributes() {
61
59
  return {
62
- listType: {
63
- default: undefined,
64
- },
65
60
  blockColor: {
66
61
  default: undefined,
67
62
  },
68
63
  blockStyle: {
69
64
  default: undefined,
70
65
  },
71
- headingType: {
72
- default: undefined,
73
- keepOnSplit: false,
74
- },
75
66
  };
76
67
  },
77
68
 
78
69
  parseHTML() {
79
70
  return [
80
- // For parsing blocks within the editor.
81
71
  {
82
72
  tag: "div",
83
73
  getAttrs: (element) => {
@@ -96,49 +86,6 @@ export const Block = Node.create<IBlock>({
96
86
  return attrs;
97
87
  }
98
88
 
99
- return false;
100
- },
101
- },
102
- // For parsing headings & paragraphs copied from outside the editor.
103
- {
104
- tag: "p",
105
- priority: 100,
106
- },
107
- {
108
- tag: "h1",
109
- attrs: { headingType: "1" },
110
- },
111
- {
112
- tag: "h2",
113
- attrs: { headingType: "2" },
114
- },
115
- {
116
- tag: "h3",
117
- attrs: { headingType: "3" },
118
- },
119
- // For parsing list items copied from outside the editor.
120
- {
121
- tag: "li",
122
- getAttrs: (element) => {
123
- if (typeof element === "string") {
124
- return false;
125
- }
126
-
127
- const parent = element.parentElement;
128
-
129
- if (parent === null) {
130
- return false;
131
- }
132
-
133
- // Gets type of list item (ordered/unordered) based on parent element's tag ("ol"/"ul").
134
- if (parent.tagName === "UL") {
135
- return { listType: "li" };
136
- }
137
-
138
- if (parent.tagName === "OL") {
139
- return { listType: "oli" };
140
- }
141
-
142
89
  return false;
143
90
  },
144
91
  },
@@ -148,7 +95,10 @@ export const Block = Node.create<IBlock>({
148
95
  renderHTML({ HTMLAttributes }) {
149
96
  const attrs: Record<string, string> = {};
150
97
  for (let [nodeAttr, HTMLAttr] of Object.entries(BlockAttributes)) {
151
- attrs[HTMLAttr] = HTMLAttributes[nodeAttr];
98
+ // Ensure falsy values are not misinterpreted.
99
+ if (HTMLAttributes[nodeAttr] !== undefined) {
100
+ attrs[HTMLAttr] = HTMLAttributes[nodeAttr];
101
+ }
152
102
  }
153
103
 
154
104
  return [
@@ -160,275 +110,448 @@ export const Block = Node.create<IBlock>({
160
110
  [
161
111
  "div",
162
112
  mergeAttributes(attrs, {
113
+ // TODO: maybe remove html attributes from inner block
163
114
  class: styles.block,
164
- "data-node-type": "block",
115
+ "data-node-type": this.name,
165
116
  }),
166
117
  0,
167
118
  ],
168
119
  ];
169
120
  },
170
121
 
171
- addInputRules() {
172
- return [
173
- ...["1", "2", "3"].map((level) => {
174
- // Create a heading when starting with "#", "##", or "###""
175
- return textblockTypeInputRuleSameNodeType({
176
- find: new RegExp(`^(#{1,${level}})\\s$`),
177
- type: this.type,
178
- getAttributes: {
179
- headingType: level,
180
- },
181
- });
182
- }),
183
- // Create a list when starting with "-"
184
- textblockTypeInputRuleSameNodeType({
185
- find: /^\s*([-+*])\s$/,
186
- type: this.type,
187
- getAttributes: {
188
- listType: "li",
189
- },
190
- }),
191
- textblockTypeInputRuleSameNodeType({
192
- find: new RegExp(/^1.\s/),
193
- type: this.type,
194
- getAttributes: {
195
- listType: "oli",
196
- },
197
- }),
198
- ];
199
- },
200
-
201
122
  addCommands() {
202
123
  return {
203
- setBlockHeading:
204
- (attributes) =>
205
- ({ tr, dispatch }) => {
206
- return setBlockHeading(tr, dispatch, attributes.level);
124
+ // Creates a new text block at a given position.
125
+ BNCreateBlock:
126
+ (pos) =>
127
+ ({ state, dispatch }) => {
128
+ const newBlock = state.schema.nodes["block"].createAndFill()!;
129
+
130
+ if (dispatch) {
131
+ state.tr.insert(pos, newBlock);
132
+ }
133
+
134
+ return true;
207
135
  },
208
- unsetBlockHeading:
209
- () =>
210
- ({ tr, dispatch }) => {
211
- return setBlockHeading(tr, dispatch, undefined);
136
+ // Deletes a block at a given position and sets the selection to where the block was.
137
+ BNDeleteBlock:
138
+ (posInBlock) =>
139
+ ({ state, view, dispatch }) => {
140
+ const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
141
+ if (blockInfo === undefined) {
142
+ return false;
143
+ }
144
+
145
+ const { startPos, endPos } = blockInfo;
146
+
147
+ if (dispatch) {
148
+ state.tr.deleteRange(startPos, endPos);
149
+ state.tr.setSelection(
150
+ new TextSelection(state.doc.resolve(startPos + 1))
151
+ );
152
+ view.focus();
153
+ }
154
+
155
+ return true;
212
156
  },
213
- unsetList:
214
- () =>
215
- ({ tr, dispatch }) => {
216
- const node = tr.selection.$anchor.node(-1);
217
- const nodePos = tr.selection.$anchor.posAtIndex(0, -1) - 1;
157
+ // Appends the text contents of a block to the nearest previous block, given a position between them. Children of
158
+ // the merged block are moved out of it first, rather than also being merged.
159
+ //
160
+ // In the example below, the position passed into the function is between Block1 and Block2.
161
+ //
162
+ // Block1
163
+ // Block2
164
+ // Block3
165
+ // Block4
166
+ // Block5
167
+ //
168
+ // Becomes:
169
+ //
170
+ // Block1
171
+ // Block2Block3
172
+ // Block4
173
+ // Block5
174
+ BNMergeBlocks:
175
+ (posBetweenBlocks) =>
176
+ ({ state, dispatch }) => {
177
+ const nextNodeIsBlock =
178
+ state.doc.resolve(posBetweenBlocks + 1).node().type.name ===
179
+ "block";
180
+ const prevNodeIsBlock =
181
+ state.doc.resolve(posBetweenBlocks - 1).node().type.name ===
182
+ "block";
183
+
184
+ if (!nextNodeIsBlock || !prevNodeIsBlock) {
185
+ return false;
186
+ }
187
+
188
+ const nextBlockInfo = getBlockInfoFromPos(
189
+ state.doc,
190
+ posBetweenBlocks + 1
191
+ );
192
+
193
+ const { node, contentNode, startPos, endPos, depth } = nextBlockInfo!;
194
+
195
+ // Removes a level of nesting all children of the next block by 1 level, if it contains both content and block
196
+ // group nodes.
197
+ if (node.childCount === 2) {
198
+ const childBlocksStart = state.doc.resolve(
199
+ startPos + contentNode.nodeSize + 1
200
+ );
201
+ const childBlocksEnd = state.doc.resolve(endPos - 1);
202
+ const childBlocksRange =
203
+ childBlocksStart.blockRange(childBlocksEnd);
218
204
 
219
- // const node2 = tr.doc.nodeAt(nodePos);
220
- if (node.type.name === "block" && node.attrs["listType"]) {
205
+ // Moves the block group node inside the block into the block group node that the current block is in.
221
206
  if (dispatch) {
222
- tr.setNodeMarkup(nodePos, undefined, {
223
- ...node.attrs,
224
- listType: undefined,
225
- });
226
- return true;
207
+ state.tr.lift(childBlocksRange!, depth - 1);
227
208
  }
228
209
  }
229
- return false;
230
- },
231
210
 
232
- addNewBlockAsSibling:
233
- (attributes) =>
234
- ({ tr, dispatch, state }) => {
235
- // Get current block
236
- const currentBlock = findBlock(tr.selection);
237
- if (!currentBlock) {
211
+ let prevBlockEndPos = posBetweenBlocks - 1;
212
+ let prevBlockInfo = getBlockInfoFromPos(state.doc, prevBlockEndPos);
213
+
214
+ // Finds the nearest previous block, regardless of nesting level.
215
+ while (prevBlockInfo!.numChildBlocks > 0) {
216
+ prevBlockEndPos--;
217
+ prevBlockInfo = getBlockInfoFromPos(state.doc, prevBlockEndPos);
218
+ if (prevBlockInfo === undefined) {
219
+ return false;
220
+ }
221
+ }
222
+
223
+ // Deletes next block and adds its text content to the nearest previous block.
224
+ // TODO: Is there any situation where we need the whole block content, not just text? Implementation for this
225
+ // is trickier.
226
+ if (dispatch) {
227
+ state.tr.deleteRange(startPos, startPos + contentNode.nodeSize);
228
+ state.tr.insertText(contentNode.textContent, prevBlockEndPos - 1);
229
+ state.tr.setSelection(
230
+ new TextSelection(state.doc.resolve(prevBlockEndPos - 1))
231
+ );
232
+ }
233
+
234
+ return true;
235
+ },
236
+ // Splits a block at a given position. Content after the position is moved to a new block below, at the same
237
+ // nesting level.
238
+ BNSplitBlock:
239
+ (posInBlock, keepType) =>
240
+ ({ state, dispatch }) => {
241
+ const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
242
+ if (blockInfo === undefined) {
238
243
  return false;
239
244
  }
240
245
 
241
- // If current blocks content is empty dont create a new block
242
- if (currentBlock.node.firstChild?.textContent.length === 0) {
243
- if (dispatch) {
244
- tr.setNodeMarkup(currentBlock.pos, undefined, attributes);
246
+ const { contentNode, contentType, startPos, endPos, depth } =
247
+ blockInfo;
248
+
249
+ const newBlockInsertionPos = endPos + 1;
250
+
251
+ // Creates new block first, otherwise positions get changed due to the original block's content changing.
252
+ // Only text content is transferred to the new block.
253
+ const secondBlockContent = state.doc.textBetween(posInBlock, endPos);
254
+
255
+ const newBlock = state.schema.nodes["block"].createAndFill()!;
256
+ const newBlockContentPos = newBlockInsertionPos + 2;
257
+
258
+ if (dispatch) {
259
+ state.tr.insert(newBlockInsertionPos, newBlock);
260
+ state.tr.insertText(secondBlockContent, newBlockContentPos);
261
+
262
+ if (keepType) {
263
+ state.tr.setBlockType(
264
+ newBlockContentPos,
265
+ newBlockContentPos,
266
+ state.schema.node(contentType).type,
267
+ contentNode.attrs
268
+ );
245
269
  }
246
- return true;
247
270
  }
248
271
 
249
- // Create new block after current block
250
- const endOfBlock = currentBlock.pos + currentBlock.node.nodeSize;
251
- let newBlock = state.schema.nodes["block"].createAndFill(attributes)!;
272
+ // Updates content of original block.
273
+ const firstBlockContent = state.doc.content.cut(startPos, posInBlock);
274
+
252
275
  if (dispatch) {
253
- tr.insert(endOfBlock, newBlock);
254
- tr.setSelection(new TextSelection(tr.doc.resolve(endOfBlock + 1)));
276
+ state.tr.replace(
277
+ startPos,
278
+ endPos,
279
+ new Slice(firstBlockContent, depth, depth)
280
+ );
255
281
  }
282
+
256
283
  return true;
257
284
  },
258
- setBlockList:
259
- (type) =>
260
- ({ tr, dispatch }) => {
261
- const node = tr.selection.$anchor.node(-1);
262
- const nodePos = tr.selection.$anchor.posAtIndex(0, -1) - 1;
263
-
264
- // const node2 = tr.doc.nodeAt(nodePos);
265
- if (node.type.name === "block") {
266
- if (dispatch) {
267
- tr.setNodeMarkup(nodePos, undefined, {
268
- ...node.attrs,
269
- listType: type,
270
- });
271
- }
272
- return true;
285
+ // Changes the content of a block at a given position to a given type.
286
+ BNSetContentType:
287
+ (posInBlock, type) =>
288
+ ({ state, dispatch }) => {
289
+ const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
290
+ if (blockInfo === undefined) {
291
+ return false;
292
+ }
293
+
294
+ const { startPos, contentNode } = blockInfo;
295
+
296
+ if (dispatch) {
297
+ state.tr.setBlockType(
298
+ startPos + 1,
299
+ startPos + contentNode.nodeSize + 1,
300
+ state.schema.node(type.name).type,
301
+ type.attrs
302
+ );
303
+ }
304
+
305
+ return true;
306
+ },
307
+ // Changes the block at a given position to a given content type if it's empty, otherwise creates a new block of
308
+ // that type below it.
309
+ BNCreateBlockOrSetContentType:
310
+ (posInBlock, type) =>
311
+ ({ state, chain }) => {
312
+ const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
313
+ if (blockInfo === undefined) {
314
+ return false;
315
+ }
316
+
317
+ const { node, startPos, endPos } = blockInfo;
318
+
319
+ if (node.textContent.length === 0) {
320
+ const oldBlockContentPos = startPos + 1;
321
+
322
+ return chain()
323
+ .BNSetContentType(posInBlock, type)
324
+ .setTextSelection(oldBlockContentPos)
325
+ .run();
326
+ } else {
327
+ const newBlockInsertionPos = endPos + 1;
328
+ const newBlockContentPos = newBlockInsertionPos + 1;
329
+
330
+ return chain()
331
+ .BNCreateBlock(newBlockInsertionPos)
332
+ .BNSetContentType(newBlockContentPos, type)
333
+ .setTextSelection(newBlockContentPos)
334
+ .run();
273
335
  }
274
- return false;
275
336
  },
276
- joinBackward:
277
- () =>
278
- ({ view, dispatch, state }) =>
279
- joinBackward(state, dispatch, view), // Override default joinBackward with edited command
280
337
  };
281
338
  },
339
+
282
340
  addProseMirrorPlugins() {
283
- return [PreviousBlockTypePlugin(), OrderedListPlugin()];
341
+ return [PreviousBlockTypePlugin()];
284
342
  },
343
+
285
344
  addKeyboardShortcuts() {
286
345
  // handleBackspace is partially adapted from https://github.com/ueberdosis/tiptap/blob/ed56337470efb4fd277128ab7ef792b37cfae992/packages/core/src/extensions/keymap.ts
287
346
  const handleBackspace = () =>
288
347
  this.editor.commands.first(({ commands }) => [
289
- // Maybe the user wants to undo an auto formatting input rule (e.g.: - or #, and then hit backspace) (source: tiptap)
348
+ // Deletes the selection if it's not empty.
349
+ () => commands.deleteSelection(),
350
+ // Undoes an input rule if one was triggered in the last editor state change.
290
351
  () => commands.undoInputRule(),
291
- // maybe convert first text block node to default node (source: tiptap)
352
+ // Changes block type to a text block if it's not already, while the selection is at the start of the block.
353
+ () =>
354
+ commands.command(({ state }) => {
355
+ const { contentType } = getBlockInfoFromPos(
356
+ state.doc,
357
+ state.selection.from
358
+ )!;
359
+
360
+ const selectionAtBlockStart =
361
+ state.selection.$anchor.parentOffset === 0;
362
+ const isTextBlock = contentType.name === "textContent";
363
+
364
+ if (selectionAtBlockStart && !isTextBlock) {
365
+ return commands.BNSetContentType(state.selection.from, {
366
+ name: "textContent",
367
+ });
368
+ }
369
+
370
+ return false;
371
+ }),
372
+ // Removes a level of nesting if the block is indented if the selection is at the start of the block.
292
373
  () =>
293
- commands.command(({ tr }) => {
294
- const { selection, doc } = tr;
295
- const { empty, $anchor } = selection;
296
- const { pos, parent } = $anchor;
297
- const isAtStart = Selection.atStart(doc).from === pos;
374
+ commands.command(({ state }) => {
375
+ const selectionAtBlockStart =
376
+ state.selection.$anchor.parentOffset === 0;
298
377
 
299
- if (
300
- !empty ||
301
- !isAtStart ||
302
- !parent.type.isTextblock ||
303
- parent.textContent.length
304
- ) {
305
- return false;
378
+ if (selectionAtBlockStart) {
379
+ return commands.liftListItem("block");
306
380
  }
307
381
 
308
- return commands.clearNodes();
382
+ return false;
309
383
  }),
310
- () => commands.deleteSelection(), // (source: tiptap)
384
+ // Merges block with the previous one if it isn't indented, isn't the first block in the doc, and the selection
385
+ // is at the start of the block.
311
386
  () =>
312
- commands.command(({ tr }) => {
313
- const isAtStartOfNode = tr.selection.$anchor.parentOffset === 0;
314
- const node = tr.selection.$anchor.node(-1);
315
- if (isAtStartOfNode && node.type.name === "block") {
316
- // we're at the start of the block, so we're trying to "backspace" the bullet or indentation
317
- return commands.first([
318
- () => commands.unsetList(), // first try to remove the "list" property
319
- () => commands.liftListItem("block"), // then try to remove a level of indentation
320
- ]);
387
+ commands.command(({ state }) => {
388
+ const { depth, startPos } = getBlockInfoFromPos(
389
+ state.doc,
390
+ state.selection.from
391
+ )!;
392
+
393
+ const selectionAtBlockStart =
394
+ state.selection.$anchor.parentOffset === 0;
395
+ const selectionEmpty =
396
+ state.selection.anchor === state.selection.head;
397
+ const blockAtDocStart = startPos === 2;
398
+
399
+ const posBetweenBlocks = startPos - 1;
400
+
401
+ if (
402
+ !blockAtDocStart &&
403
+ selectionAtBlockStart &&
404
+ selectionEmpty &&
405
+ depth === 2
406
+ ) {
407
+ return commands.BNMergeBlocks(posBetweenBlocks);
321
408
  }
409
+
322
410
  return false;
323
411
  }),
324
- ({ chain }) =>
325
- // we are at the start of a block at the root level. The user hits backspace to "merge it" to the end of the block above
326
- //
327
- // BlockA
328
- // BlockB
329
-
330
- // Becomes:
331
-
332
- // BlockABlockB
333
-
334
- chain()
335
- .command(({ tr, state, dispatch }) => {
336
- const isAtStartOfNode = tr.selection.$anchor.parentOffset === 0;
337
- const anchor = tr.selection.$anchor;
338
- const node = anchor.node(-1);
339
- if (isAtStartOfNode && node.type.name === "block") {
340
- if (node.childCount === 2) {
341
- // BlockB has children. We want to go from this:
342
- //
343
- // BlockA
344
- // BlockB
345
- // BlockC
346
- // BlockD
347
- //
348
- // to:
349
- //
350
- // BlockABlockB
351
- // BlockC
352
- // BlockD
353
-
354
- // This parts moves the children of BlockB to the top level
355
- const startSecondChild = anchor.posAtIndex(1, -1) + 1; // start of blockgroup
356
- const endSecondChild = anchor.posAtIndex(2, -1) - 1;
357
- const range = state.doc
358
- .resolve(startSecondChild)
359
- .blockRange(state.doc.resolve(endSecondChild));
360
-
361
- if (dispatch) {
362
- tr.lift(range!, anchor.depth - 2);
363
- }
364
- }
365
- return true;
366
- }
367
- return false;
368
- })
369
- // use joinBackward to merge BlockB to BlockA (i.e.: turn it into BlockABlockB)
370
- // The standard JoinBackward would break here, and would turn it into:
371
- // BlockA
372
- // BlockB
373
- //
374
- // joinBackward has been patched with our custom version to fix this (see commands/joinBackward)
375
- .joinBackward()
376
- .run(),
377
-
378
- () => commands.selectNodeBackward(), // (source: tiptap)
379
412
  ]);
380
413
 
381
414
  const handleEnter = () =>
382
415
  this.editor.commands.first(({ commands }) => [
383
- // Try to split the current block into 2 items:
384
- () => commands.splitListItem("block"),
385
- // Otherwise, maybe we are in an empty list item. "Enter" should remove the list bullet
386
- ({ tr, dispatch }) => {
387
- const $from = tr.selection.$from;
388
- if ($from.depth !== 3) {
389
- // only needed at root level, at deeper levels it should be handled already by splitListItem
416
+ // Removes a level of nesting if the block is empty & indented, while the selection is also empty & at the start
417
+ // of the block.
418
+ () =>
419
+ commands.command(({ state }) => {
420
+ const { node, depth } = getBlockInfoFromPos(
421
+ state.doc,
422
+ state.selection.from
423
+ )!;
424
+
425
+ const selectionAtBlockStart =
426
+ state.selection.$anchor.parentOffset === 0;
427
+ const selectionEmpty =
428
+ state.selection.anchor === state.selection.head;
429
+ const blockEmpty = node.textContent.length === 0;
430
+ const blockIndented = depth > 2;
431
+
432
+ if (
433
+ selectionAtBlockStart &&
434
+ selectionEmpty &&
435
+ blockEmpty &&
436
+ blockIndented
437
+ ) {
438
+ return commands.liftListItem("block");
439
+ }
440
+
390
441
  return false;
391
- }
392
- const node = tr.selection.$anchor.node(-1);
393
- const nodePos = tr.selection.$anchor.posAtIndex(0, -1) - 1;
442
+ }),
443
+ // Creates a new block and moves the selection to it if the current one is empty, while the selection is also
444
+ // empty & at the start of the block.
445
+ () =>
446
+ commands.command(({ state, chain }) => {
447
+ const { node, endPos } = getBlockInfoFromPos(
448
+ state.doc,
449
+ state.selection.from
450
+ )!;
451
+
452
+ const selectionAtBlockStart =
453
+ state.selection.$anchor.parentOffset === 0;
454
+ const selectionEmpty =
455
+ state.selection.anchor === state.selection.head;
456
+ const blockEmpty = node.textContent.length === 0;
457
+
458
+ if (selectionAtBlockStart && selectionEmpty && blockEmpty) {
459
+ const newBlockInsertionPos = endPos + 1;
460
+ const newBlockContentPos = newBlockInsertionPos + 2;
461
+
462
+ chain()
463
+ .BNCreateBlock(newBlockInsertionPos)
464
+ .setTextSelection(newBlockContentPos)
465
+ .run();
394
466
 
395
- if (node.type.name === "block" && node.attrs["listType"]) {
396
- if (dispatch) {
397
- tr.setNodeMarkup(nodePos, undefined, {
398
- ...node.attrs,
399
- listType: undefined,
400
- });
467
+ return true;
401
468
  }
402
- return true;
403
- }
404
- return false;
405
- },
406
- // Otherwise, we might be on an empty line and hit "Enter" to create a new line:
407
- ({ tr, dispatch }) => {
408
- const $from = tr.selection.$from;
409
469
 
410
- if (dispatch) {
411
- tr.split($from.pos, 2).scrollIntoView();
412
- }
413
- return true;
414
- },
470
+ return false;
471
+ }),
472
+ // Splits the current block, moving content inside that's after the cursor to a new text block below. Also
473
+ // deletes the selection beforehand, if it's not empty.
474
+ () =>
475
+ commands.command(({ state, chain }) => {
476
+ const { node } = getBlockInfoFromPos(
477
+ state.doc,
478
+ state.selection.from
479
+ )!;
480
+
481
+ const blockEmpty = node.textContent.length === 0;
482
+
483
+ if (!blockEmpty) {
484
+ chain()
485
+ .deleteSelection()
486
+ .BNSplitBlock(state.selection.from, false)
487
+ .run();
488
+
489
+ return true;
490
+ }
491
+
492
+ return false;
493
+ }),
415
494
  ]);
416
495
 
417
496
  return {
418
497
  Backspace: handleBackspace,
419
498
  Enter: handleEnter,
420
499
  Tab: () => this.editor.commands.sinkListItem("block"),
421
- "Shift-Tab": () => {
422
- return this.editor.commands.liftListItem("block");
423
- },
500
+ "Shift-Tab": () => this.editor.commands.liftListItem("block"),
424
501
  "Mod-Alt-0": () =>
425
- this.editor.chain().unsetList().unsetBlockHeading().run(),
426
- "Mod-Alt-1": () => this.editor.commands.setBlockHeading({ level: "1" }),
427
- "Mod-Alt-2": () => this.editor.commands.setBlockHeading({ level: "2" }),
428
- "Mod-Alt-3": () => this.editor.commands.setBlockHeading({ level: "3" }),
429
- "Mod-Shift-7": () => this.editor.commands.setBlockList("li"),
430
- "Mod-Shift-8": () => this.editor.commands.setBlockList("oli"),
431
- // TODO: Add shortcuts for numbered and bullet list
502
+ this.editor.commands.BNCreateBlock(
503
+ this.editor.state.selection.anchor + 2
504
+ ),
505
+ "Mod-Alt-1": () =>
506
+ this.editor.commands.BNSetContentType(
507
+ this.editor.state.selection.anchor,
508
+ {
509
+ name: "headingContent",
510
+ attrs: {
511
+ headingLevel: "1",
512
+ },
513
+ }
514
+ ),
515
+ "Mod-Alt-2": () =>
516
+ this.editor.commands.BNSetContentType(
517
+ this.editor.state.selection.anchor,
518
+ {
519
+ name: "headingContent",
520
+ attrs: {
521
+ headingLevel: "2",
522
+ },
523
+ }
524
+ ),
525
+ "Mod-Alt-3": () =>
526
+ this.editor.commands.BNSetContentType(
527
+ this.editor.state.selection.anchor,
528
+ {
529
+ name: "headingContent",
530
+ attrs: {
531
+ headingLevel: "3",
532
+ },
533
+ }
534
+ ),
535
+ "Mod-Shift-7": () =>
536
+ this.editor.commands.BNSetContentType(
537
+ this.editor.state.selection.anchor,
538
+ {
539
+ name: "listItemContent",
540
+ attrs: {
541
+ listItemType: "unordered",
542
+ },
543
+ }
544
+ ),
545
+ "Mod-Shift-8": () =>
546
+ this.editor.commands.BNSetContentType(
547
+ this.editor.state.selection.anchor,
548
+ {
549
+ name: "listItemContent",
550
+ attrs: {
551
+ listItemType: "ordered",
552
+ },
553
+ }
554
+ ),
432
555
  };
433
556
  },
434
557
  });