@blocknote/core 0.1.2 → 0.2.1-alpha.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 -5109
  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 +19 -32
  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 +413 -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 +5 -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 -1
  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,8 @@ 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
- if (HTMLAttributes[nodeAttr]) {
98
+ // Ensure falsy values are not misinterpreted.
99
+ if (HTMLAttributes[nodeAttr] !== undefined) {
152
100
  attrs[HTMLAttr] = HTMLAttributes[nodeAttr];
153
101
  }
154
102
  }
@@ -162,275 +110,448 @@ export const Block = Node.create<IBlock>({
162
110
  [
163
111
  "div",
164
112
  mergeAttributes(attrs, {
113
+ // TODO: maybe remove html attributes from inner block
165
114
  class: styles.block,
166
- "data-node-type": "block",
115
+ "data-node-type": this.name,
167
116
  }),
168
117
  0,
169
118
  ],
170
119
  ];
171
120
  },
172
121
 
173
- addInputRules() {
174
- return [
175
- ...["1", "2", "3"].map((level) => {
176
- // Create a heading when starting with "#", "##", or "###""
177
- return textblockTypeInputRuleSameNodeType({
178
- find: new RegExp(`^(#{1,${level}})\\s$`),
179
- type: this.type,
180
- getAttributes: {
181
- headingType: level,
182
- },
183
- });
184
- }),
185
- // Create a list when starting with "-"
186
- textblockTypeInputRuleSameNodeType({
187
- find: /^\s*([-+*])\s$/,
188
- type: this.type,
189
- getAttributes: {
190
- listType: "li",
191
- },
192
- }),
193
- textblockTypeInputRuleSameNodeType({
194
- find: new RegExp(/^1.\s/),
195
- type: this.type,
196
- getAttributes: {
197
- listType: "oli",
198
- },
199
- }),
200
- ];
201
- },
202
-
203
122
  addCommands() {
204
123
  return {
205
- setBlockHeading:
206
- (attributes) =>
207
- ({ tr, dispatch }) => {
208
- 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;
209
135
  },
210
- unsetBlockHeading:
211
- () =>
212
- ({ tr, dispatch }) => {
213
- 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;
214
156
  },
215
- unsetList:
216
- () =>
217
- ({ tr, dispatch }) => {
218
- const node = tr.selection.$anchor.node(-1);
219
- 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);
220
204
 
221
- // const node2 = tr.doc.nodeAt(nodePos);
222
- 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.
223
206
  if (dispatch) {
224
- tr.setNodeMarkup(nodePos, undefined, {
225
- ...node.attrs,
226
- listType: undefined,
227
- });
228
- return true;
207
+ state.tr.lift(childBlocksRange!, depth - 1);
229
208
  }
230
209
  }
231
- return false;
232
- },
233
210
 
234
- addNewBlockAsSibling:
235
- (attributes) =>
236
- ({ tr, dispatch, state }) => {
237
- // Get current block
238
- const currentBlock = findBlock(tr.selection);
239
- 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) {
240
243
  return false;
241
244
  }
242
245
 
243
- // If current blocks content is empty dont create a new block
244
- if (currentBlock.node.firstChild?.textContent.length === 0) {
245
- if (dispatch) {
246
- 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
+ );
247
269
  }
248
- return true;
249
270
  }
250
271
 
251
- // Create new block after current block
252
- const endOfBlock = currentBlock.pos + currentBlock.node.nodeSize;
253
- let newBlock = state.schema.nodes["block"].createAndFill(attributes)!;
272
+ // Updates content of original block.
273
+ const firstBlockContent = state.doc.content.cut(startPos, posInBlock);
274
+
254
275
  if (dispatch) {
255
- tr.insert(endOfBlock, newBlock);
256
- tr.setSelection(new TextSelection(tr.doc.resolve(endOfBlock + 1)));
276
+ state.tr.replace(
277
+ startPos,
278
+ endPos,
279
+ new Slice(firstBlockContent, depth, depth)
280
+ );
257
281
  }
282
+
258
283
  return true;
259
284
  },
260
- setBlockList:
261
- (type) =>
262
- ({ tr, dispatch }) => {
263
- const node = tr.selection.$anchor.node(-1);
264
- const nodePos = tr.selection.$anchor.posAtIndex(0, -1) - 1;
265
-
266
- // const node2 = tr.doc.nodeAt(nodePos);
267
- if (node.type.name === "block") {
268
- if (dispatch) {
269
- tr.setNodeMarkup(nodePos, undefined, {
270
- ...node.attrs,
271
- listType: type,
272
- });
273
- }
274
- 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();
275
335
  }
276
- return false;
277
336
  },
278
- joinBackward:
279
- () =>
280
- ({ view, dispatch, state }) =>
281
- joinBackward(state, dispatch, view), // Override default joinBackward with edited command
282
337
  };
283
338
  },
339
+
284
340
  addProseMirrorPlugins() {
285
- return [PreviousBlockTypePlugin(), OrderedListPlugin()];
341
+ return [PreviousBlockTypePlugin()];
286
342
  },
343
+
287
344
  addKeyboardShortcuts() {
288
345
  // handleBackspace is partially adapted from https://github.com/ueberdosis/tiptap/blob/ed56337470efb4fd277128ab7ef792b37cfae992/packages/core/src/extensions/keymap.ts
289
346
  const handleBackspace = () =>
290
347
  this.editor.commands.first(({ commands }) => [
291
- // 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.
292
351
  () => commands.undoInputRule(),
293
- // 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.
294
373
  () =>
295
- commands.command(({ tr }) => {
296
- const { selection, doc } = tr;
297
- const { empty, $anchor } = selection;
298
- const { pos, parent } = $anchor;
299
- const isAtStart = Selection.atStart(doc).from === pos;
374
+ commands.command(({ state }) => {
375
+ const selectionAtBlockStart =
376
+ state.selection.$anchor.parentOffset === 0;
300
377
 
301
- if (
302
- !empty ||
303
- !isAtStart ||
304
- !parent.type.isTextblock ||
305
- parent.textContent.length
306
- ) {
307
- return false;
378
+ if (selectionAtBlockStart) {
379
+ return commands.liftListItem("block");
308
380
  }
309
381
 
310
- return commands.clearNodes();
382
+ return false;
311
383
  }),
312
- () => 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.
313
386
  () =>
314
- commands.command(({ tr }) => {
315
- const isAtStartOfNode = tr.selection.$anchor.parentOffset === 0;
316
- const node = tr.selection.$anchor.node(-1);
317
- if (isAtStartOfNode && node.type.name === "block") {
318
- // we're at the start of the block, so we're trying to "backspace" the bullet or indentation
319
- return commands.first([
320
- () => commands.unsetList(), // first try to remove the "list" property
321
- () => commands.liftListItem("block"), // then try to remove a level of indentation
322
- ]);
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);
323
408
  }
409
+
324
410
  return false;
325
411
  }),
326
- ({ chain }) =>
327
- // 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
328
- //
329
- // BlockA
330
- // BlockB
331
-
332
- // Becomes:
333
-
334
- // BlockABlockB
335
-
336
- chain()
337
- .command(({ tr, state, dispatch }) => {
338
- const isAtStartOfNode = tr.selection.$anchor.parentOffset === 0;
339
- const anchor = tr.selection.$anchor;
340
- const node = anchor.node(-1);
341
- if (isAtStartOfNode && node.type.name === "block") {
342
- if (node.childCount === 2) {
343
- // BlockB has children. We want to go from this:
344
- //
345
- // BlockA
346
- // BlockB
347
- // BlockC
348
- // BlockD
349
- //
350
- // to:
351
- //
352
- // BlockABlockB
353
- // BlockC
354
- // BlockD
355
-
356
- // This parts moves the children of BlockB to the top level
357
- const startSecondChild = anchor.posAtIndex(1, -1) + 1; // start of blockgroup
358
- const endSecondChild = anchor.posAtIndex(2, -1) - 1;
359
- const range = state.doc
360
- .resolve(startSecondChild)
361
- .blockRange(state.doc.resolve(endSecondChild));
362
-
363
- if (dispatch) {
364
- tr.lift(range!, anchor.depth - 2);
365
- }
366
- }
367
- return true;
368
- }
369
- return false;
370
- })
371
- // use joinBackward to merge BlockB to BlockA (i.e.: turn it into BlockABlockB)
372
- // The standard JoinBackward would break here, and would turn it into:
373
- // BlockA
374
- // BlockB
375
- //
376
- // joinBackward has been patched with our custom version to fix this (see commands/joinBackward)
377
- .joinBackward()
378
- .run(),
379
-
380
- () => commands.selectNodeBackward(), // (source: tiptap)
381
412
  ]);
382
413
 
383
414
  const handleEnter = () =>
384
415
  this.editor.commands.first(({ commands }) => [
385
- // Try to split the current block into 2 items:
386
- () => commands.splitListItem("block"),
387
- // Otherwise, maybe we are in an empty list item. "Enter" should remove the list bullet
388
- ({ tr, dispatch }) => {
389
- const $from = tr.selection.$from;
390
- if ($from.depth !== 3) {
391
- // 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
+
392
441
  return false;
393
- }
394
- const node = tr.selection.$anchor.node(-1);
395
- 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();
396
466
 
397
- if (node.type.name === "block" && node.attrs["listType"]) {
398
- if (dispatch) {
399
- tr.setNodeMarkup(nodePos, undefined, {
400
- ...node.attrs,
401
- listType: undefined,
402
- });
467
+ return true;
403
468
  }
404
- return true;
405
- }
406
- return false;
407
- },
408
- // Otherwise, we might be on an empty line and hit "Enter" to create a new line:
409
- ({ tr, dispatch }) => {
410
- const $from = tr.selection.$from;
411
469
 
412
- if (dispatch) {
413
- tr.split($from.pos, 2).scrollIntoView();
414
- }
415
- return true;
416
- },
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
+ }),
417
494
  ]);
418
495
 
419
496
  return {
420
497
  Backspace: handleBackspace,
421
498
  Enter: handleEnter,
422
499
  Tab: () => this.editor.commands.sinkListItem("block"),
423
- "Shift-Tab": () => {
424
- return this.editor.commands.liftListItem("block");
425
- },
500
+ "Shift-Tab": () => this.editor.commands.liftListItem("block"),
426
501
  "Mod-Alt-0": () =>
427
- this.editor.chain().unsetList().unsetBlockHeading().run(),
428
- "Mod-Alt-1": () => this.editor.commands.setBlockHeading({ level: "1" }),
429
- "Mod-Alt-2": () => this.editor.commands.setBlockHeading({ level: "2" }),
430
- "Mod-Alt-3": () => this.editor.commands.setBlockHeading({ level: "3" }),
431
- "Mod-Shift-7": () => this.editor.commands.setBlockList("li"),
432
- "Mod-Shift-8": () => this.editor.commands.setBlockList("oli"),
433
- // 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
+ ),
434
555
  };
435
556
  },
436
557
  });