@blocknote/core 0.1.0 → 0.1.2

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 (99) hide show
  1. package/README.md +1 -1
  2. package/dist/blocknote.js +3454 -2426
  3. package/dist/blocknote.js.map +1 -1
  4. package/dist/blocknote.umd.cjs +35 -71
  5. package/dist/blocknote.umd.cjs.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +7 -6
  8. package/src/BlockNoteTheme.ts +150 -0
  9. package/src/extensions/Blocks/BlockAttributes.ts +12 -0
  10. package/src/extensions/Blocks/MultipleNodeSelection.ts +87 -0
  11. package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +8 -2
  12. package/src/extensions/Blocks/nodes/Block.module.css +37 -37
  13. package/src/extensions/Blocks/nodes/Block.ts +79 -32
  14. package/src/extensions/Blocks/nodes/BlockGroup.ts +18 -1
  15. package/src/extensions/Blocks/nodes/Content.ts +14 -1
  16. package/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +8 -1
  17. package/src/extensions/BubbleMenu/component/BubbleMenu.tsx +116 -88
  18. package/src/extensions/BubbleMenu/component/LinkToolbarButton.tsx +8 -8
  19. package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx +143 -33
  20. package/src/extensions/DraggableBlocks/components/DragHandle.tsx +14 -19
  21. package/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx +8 -7
  22. package/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +31 -66
  23. package/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.tsx +44 -0
  24. package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItem.tsx +34 -0
  25. package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemIcon.tsx +31 -0
  26. package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemInput.tsx +40 -0
  27. package/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.tsx +37 -0
  28. package/src/extensions/Hyperlinks/menus/HyperlinkMenu.tsx +63 -0
  29. package/src/extensions/SlashMenu/SlashMenuItem.ts +3 -1
  30. package/src/extensions/SlashMenu/defaultCommands.tsx +4 -4
  31. package/src/extensions/UniqueID/UniqueID.ts +0 -11
  32. package/src/shared/components/toolbar/Toolbar.tsx +8 -3
  33. package/src/shared/components/toolbar/ToolbarButton.tsx +57 -0
  34. package/src/shared/components/toolbar/ToolbarDropdown.tsx +35 -0
  35. package/src/shared/components/toolbar/ToolbarDropdownItem.tsx +35 -0
  36. package/src/shared/components/toolbar/ToolbarDropdownTarget.tsx +31 -0
  37. package/src/shared/plugins/suggestion/SuggestionItem.ts +3 -1
  38. package/src/shared/plugins/suggestion/{SuggestionListReactRenderer.ts → SuggestionListReactRenderer.tsx} +13 -4
  39. package/src/shared/plugins/suggestion/components/SuggestionGroup.tsx +6 -93
  40. package/src/shared/plugins/suggestion/components/SuggestionGroupItem.tsx +82 -0
  41. package/src/shared/plugins/suggestion/components/SuggestionList.tsx +24 -23
  42. package/src/utils.ts +12 -0
  43. package/types/src/BlockNoteTheme.d.ts +2 -0
  44. package/types/src/commands/indentation.d.ts +2 -0
  45. package/types/src/extensions/Blocks/BlockAttributes.d.ts +2 -0
  46. package/types/src/extensions/Blocks/MultipleNodeSelection.d.ts +24 -0
  47. package/types/src/extensions/Blocks/nodes/Block.d.ts +1 -1
  48. package/types/src/extensions/BubbleMenu/component/LinkToolbarButton.d.ts +2 -2
  49. package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.d.ts +11 -0
  50. package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItem.d.ts +13 -0
  51. package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemIcon.d.ts +8 -0
  52. package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemInput.d.ts +9 -0
  53. package/types/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.d.ts +12 -0
  54. package/types/src/extensions/Hyperlinks/menus/HyperlinkMenu.d.ts +21 -0
  55. package/types/src/extensions/Hyperlinks/menus/helpers/PanelTextInput.d.ts +39 -0
  56. package/types/src/extensions/Hyperlinks/menus/helpers/PanelTextInputStyles.d.ts +3 -0
  57. package/types/src/extensions/Hyperlinks/menus/helpers/ToolbarComponent.d.ts +13 -0
  58. package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +4 -7
  59. package/types/src/nodes/ChildgroupNode.d.ts +28 -0
  60. package/types/src/nodes/patchNodes.d.ts +1 -0
  61. package/types/src/plugins/TreeViewPlugin/index.d.ts +2 -0
  62. package/types/src/plugins/animation.d.ts +2 -0
  63. package/types/src/react/BlockNoteComposer.d.ts +17 -0
  64. package/types/src/react/BlockNotePlugin.d.ts +1 -0
  65. package/types/src/react/index.d.ts +3 -0
  66. package/types/src/react/useBlockNoteSetup.d.ts +2 -0
  67. package/types/src/registerBlockNote.d.ts +2 -0
  68. package/types/src/shared/components/toolbar/SimpleToolbarButton.d.ts +2 -3
  69. package/types/src/shared/components/toolbar/SimpleToolbarDropdown.d.ts +11 -0
  70. package/types/src/shared/components/toolbar/SimpleToolbarDropdownItem.d.ts +11 -0
  71. package/types/src/shared/components/toolbar/Toolbar.d.ts +2 -2
  72. package/types/src/shared/components/toolbar/ToolbarButton.d.ts +15 -0
  73. package/types/src/shared/components/toolbar/ToolbarDropdown.d.ts +17 -0
  74. package/types/src/shared/components/toolbar/ToolbarDropdownItem.d.ts +11 -0
  75. package/types/src/shared/components/toolbar/ToolbarDropdownTarget.d.ts +8 -0
  76. package/types/src/shared/plugins/suggestion/SuggestionItem.d.ts +2 -4
  77. package/types/src/shared/plugins/suggestion/components/SuggestionGroupItem.d.ts +9 -0
  78. package/types/src/shared/plugins/suggestion/components/SuggestionList.d.ts +0 -15
  79. package/types/src/themes/BlockNoteEditorTheme.d.ts +11 -0
  80. package/types/src/utils.d.ts +2 -0
  81. package/src/extensions/BubbleMenu/component/DropdownBlockItem.module.css +0 -13
  82. package/src/extensions/BubbleMenu/component/DropdownBlockItem.tsx +0 -25
  83. package/src/extensions/DraggableBlocks/components/DragHandle.module.css +0 -33
  84. package/src/extensions/DraggableBlocks/components/DragHandleMenu.module.css +0 -10
  85. package/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.tsx +0 -59
  86. package/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.tsx +0 -72
  87. package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.tsx +0 -173
  88. package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.ts +0 -36
  89. package/src/extensions/Hyperlinks/menus/atlaskit/README.md +0 -1
  90. package/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.tsx +0 -61
  91. package/src/extensions/helpers/formatKeyboardShortcut.ts +0 -9
  92. package/src/lib/atlaskit/browser.ts +0 -47
  93. package/src/shared/components/toolbar/SimpleToolbarButton.module.css +0 -13
  94. package/src/shared/components/toolbar/SimpleToolbarButton.tsx +0 -56
  95. package/src/shared/components/toolbar/Toolbar.module.css +0 -10
  96. package/src/shared/components/toolbar/ToolbarSeparator.module.css +0 -13
  97. package/src/shared/components/toolbar/ToolbarSeparator.tsx +0 -7
  98. package/src/shared/plugins/suggestion/components/SuggestionGroup.module.css +0 -45
  99. package/src/shared/plugins/suggestion/components/SuggestionList.module.css +0 -10
@@ -13,7 +13,23 @@ export const BlockGroup = Node.create({
13
13
  content: "block+",
14
14
 
15
15
  parseHTML() {
16
- return [{ tag: "div" }];
16
+ return [
17
+ {
18
+ tag: "div",
19
+ getAttrs: (element) => {
20
+ if(typeof element === "string") {
21
+ return false;
22
+ }
23
+
24
+ if(element.getAttribute("data-node-type") === "block-group") {
25
+ // Null means the element matches, but we don't want to add any attributes to the node.
26
+ return null;
27
+ }
28
+
29
+ return false;
30
+ }
31
+ }
32
+ ];
17
33
  },
18
34
 
19
35
  renderHTML({ HTMLAttributes }) {
@@ -21,6 +37,7 @@ export const BlockGroup = Node.create({
21
37
  "div",
22
38
  mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
23
39
  class: styles.blockGroup,
40
+ "data-node-type": "block-group"
24
41
  }),
25
42
  0,
26
43
  ];
@@ -32,7 +32,19 @@ export const ContentBlock = Node.create<IBlock>({
32
32
  return [
33
33
  {
34
34
  tag: "div",
35
- },
35
+ getAttrs: (element) => {
36
+ if(typeof element === "string") {
37
+ return false;
38
+ }
39
+
40
+ if(element.getAttribute("data-node-type") === "block-content") {
41
+ // Null means the element matches, but we don't want to add any attributes to the node.
42
+ return null;
43
+ }
44
+
45
+ return false;
46
+ }
47
+ }
36
48
  ];
37
49
  },
38
50
 
@@ -41,6 +53,7 @@ export const ContentBlock = Node.create<IBlock>({
41
53
  "div",
42
54
  mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
43
55
  class: styles.blockContent,
56
+ "data-node-type": "block-content"
44
57
  }),
45
58
  // TODO: The extra nested div is only needed for placeholders, different solution (without extra div) would be preferable
46
59
  // We can't use the other div because the ::before attribute on that one is already reserved for list-bullets
@@ -1,6 +1,8 @@
1
+ import { MantineProvider } from "@mantine/core";
1
2
  import { Extension } from "@tiptap/core";
2
3
  import { PluginKey } from "prosemirror-state";
3
4
  import ReactDOM from "react-dom";
5
+ import { BlockNoteTheme } from "../../BlockNoteTheme";
4
6
  import rootStyles from "../../root.module.css";
5
7
  import { createBubbleMenuPlugin } from "./BubbleMenuPlugin";
6
8
  import { BubbleMenu } from "./component/BubbleMenu";
@@ -14,7 +16,12 @@ export const BubbleMenuExtension = Extension.create<{}>({
14
16
  addProseMirrorPlugins() {
15
17
  const element = document.createElement("div");
16
18
  element.className = rootStyles.bnRoot;
17
- ReactDOM.render(<BubbleMenu editor={this.editor} />, element);
19
+ ReactDOM.render(
20
+ <MantineProvider theme={BlockNoteTheme}>
21
+ <BubbleMenu editor={this.editor} />
22
+ </MantineProvider>,
23
+ element
24
+ );
18
25
  return [
19
26
  createBubbleMenuPlugin({
20
27
  editor: this.editor,
@@ -1,4 +1,3 @@
1
- import DropdownMenu, { DropdownItemGroup } from "@atlaskit/dropdown-menu";
2
1
  import { Editor } from "@tiptap/core";
3
2
  import {
4
3
  RiBold,
@@ -15,13 +14,14 @@ import {
15
14
  RiText,
16
15
  RiUnderline,
17
16
  } from "react-icons/ri";
18
- import { SimpleToolbarButton } from "../../../shared/components/toolbar/SimpleToolbarButton";
17
+ import { ToolbarButton } from "../../../shared/components/toolbar/ToolbarButton";
18
+ import { ToolbarDropdown } from "../../../shared/components/toolbar/ToolbarDropdown";
19
19
  import { Toolbar } from "../../../shared/components/toolbar/Toolbar";
20
20
  import { useEditorForceUpdate } from "../../../shared/hooks/useEditorForceUpdate";
21
21
  import { findBlock } from "../../Blocks/helpers/findBlock";
22
- import formatKeyboardShortcut from "../../helpers/formatKeyboardShortcut";
23
- import DropdownBlockItem from "./DropdownBlockItem";
22
+ import { formatKeyboardShortcut } from "../../../utils";
24
23
  import LinkToolbarButton from "./LinkToolbarButton";
24
+ import { IconType } from "react-icons";
25
25
 
26
26
  type ListType = "li" | "oli";
27
27
 
@@ -59,123 +59,151 @@ export const BubbleMenu = (props: { editor: Editor }) => {
59
59
  currentBlockListType
60
60
  );
61
61
 
62
+ const blockIconMap: Record<string, IconType> = {
63
+ Text: RiText,
64
+ "Heading 1": RiH1,
65
+ "Heading 2": RiH2,
66
+ "Heading 3": RiH3,
67
+ "Bullet List": RiListUnordered,
68
+ "Numbered List": RiListOrdered,
69
+ };
70
+
62
71
  return (
63
72
  <Toolbar>
64
- <DropdownMenu trigger={currentBlockName}>
65
- <DropdownItemGroup>
66
- <DropdownBlockItem
67
- title="Text"
68
- icon={RiText}
69
- isSelected={currentBlockName === "Paragraph"}
70
- onClick={() =>
71
- props.editor.chain().focus().unsetBlockHeading().unsetList().run()
72
- }
73
- />
74
- <DropdownBlockItem
75
- title="Heading 1"
76
- icon={RiH1}
77
- isSelected={currentBlockName === "Heading 1"}
78
- onClick={() =>
73
+ <ToolbarDropdown
74
+ text={currentBlockName}
75
+ icon={blockIconMap[currentBlockName]}
76
+ items={[
77
+ {
78
+ onClick: () => {
79
+ // Setting editor focus using a chained command instead causes bubble menu to flicker on click.
80
+ props.editor.view.focus();
81
+ props.editor.chain().unsetBlockHeading().unsetList().run();
82
+ },
83
+ text: "Text",
84
+ icon: RiText,
85
+ isSelected: currentBlockName === "Text",
86
+ },
87
+ {
88
+ onClick: () => {
89
+ props.editor.view.focus();
79
90
  props.editor
80
91
  .chain()
81
- .focus()
82
92
  .unsetList()
83
- .setBlockHeading({ level: 1 })
84
- .run()
85
- }
86
- />
87
- <DropdownBlockItem
88
- title="Heading 2"
89
- icon={RiH2}
90
- isSelected={currentBlockName === "Heading 2"}
91
- onClick={() =>
93
+ .setBlockHeading({ level: "1" })
94
+ .run();
95
+ },
96
+ text: "Heading 1",
97
+ icon: RiH1,
98
+ isSelected: currentBlockName === "Heading 1",
99
+ },
100
+ {
101
+ onClick: () => {
102
+ props.editor.view.focus();
92
103
  props.editor
93
104
  .chain()
94
- .focus()
95
105
  .unsetList()
96
- .setBlockHeading({ level: 2 })
97
- .run()
98
- }
99
- />
100
- <DropdownBlockItem
101
- title="Heading 3"
102
- icon={RiH3}
103
- isSelected={currentBlockName === "Heading 3"}
104
- onClick={() =>
106
+ .setBlockHeading({ level: "2" })
107
+ .run();
108
+ },
109
+ text: "Heading 2",
110
+ icon: RiH2,
111
+ isSelected: currentBlockName === "Heading 2",
112
+ },
113
+ {
114
+ onClick: () => {
115
+ props.editor.view.focus();
105
116
  props.editor
106
117
  .chain()
107
- .focus()
108
118
  .unsetList()
109
- .setBlockHeading({ level: 3 })
110
- .run()
111
- }
112
- />
113
- <DropdownBlockItem
114
- title="Bullet List"
115
- icon={RiListUnordered}
116
- isSelected={currentBlockName === "Bullet List"}
117
- onClick={() =>
118
- props.editor
119
- .chain()
120
- .focus()
121
- .unsetBlockHeading()
122
- .setBlockList("li")
123
- .run()
124
- }
125
- />
126
- <DropdownBlockItem
127
- title="Numbered List"
128
- icon={RiListOrdered}
129
- isSelected={currentBlockName === "Numbered List"}
130
- onClick={() =>
119
+ .setBlockHeading({ level: "3" })
120
+ .run();
121
+ },
122
+ text: "Heading 3",
123
+ icon: RiH3,
124
+ isSelected: currentBlockName === "Heading 3",
125
+ },
126
+ {
127
+ onClick: () => {
128
+ props.editor.view.focus();
129
+ props.editor.chain().unsetBlockHeading().setBlockList("li").run();
130
+ },
131
+ text: "Bullet List",
132
+ icon: RiListUnordered,
133
+ isSelected: currentBlockName === "Bullet List",
134
+ },
135
+ {
136
+ onClick: () => {
137
+ props.editor.view.focus();
131
138
  props.editor
132
139
  .chain()
133
- .focus()
134
140
  .unsetBlockHeading()
135
141
  .setBlockList("oli")
136
- .run()
137
- }
138
- />
139
- </DropdownItemGroup>
140
- </DropdownMenu>
141
- <SimpleToolbarButton
142
- onClick={() => props.editor.chain().focus().toggleBold().run()}
142
+ .run();
143
+ },
144
+ text: "Numbered List",
145
+ icon: RiListOrdered,
146
+ isSelected: currentBlockName === "Numbered List",
147
+ },
148
+ ]}
149
+ />
150
+ <ToolbarButton
151
+ onClick={() => {
152
+ // Setting editor focus using a chained command instead causes bubble menu to flicker on click.
153
+ props.editor.view.focus();
154
+ props.editor.commands.toggleBold();
155
+ }}
143
156
  isSelected={props.editor.isActive("bold")}
144
157
  mainTooltip="Bold"
145
158
  secondaryTooltip={formatKeyboardShortcut("Mod+B")}
146
159
  icon={RiBold}
147
160
  />
148
- <SimpleToolbarButton
149
- onClick={() => props.editor.chain().focus().toggleItalic().run()}
161
+ <ToolbarButton
162
+ onClick={() => {
163
+ props.editor.view.focus();
164
+ props.editor.commands.toggleItalic();
165
+ }}
150
166
  isSelected={props.editor.isActive("italic")}
151
167
  mainTooltip="Italic"
152
168
  secondaryTooltip={formatKeyboardShortcut("Mod+I")}
153
169
  icon={RiItalic}
154
170
  />
155
- <SimpleToolbarButton
156
- onClick={() => props.editor.chain().focus().toggleUnderline().run()}
171
+ <ToolbarButton
172
+ onClick={() => {
173
+ props.editor.view.focus();
174
+ props.editor.commands.toggleUnderline();
175
+ }}
157
176
  isSelected={props.editor.isActive("underline")}
158
177
  mainTooltip="Underline"
159
178
  secondaryTooltip={formatKeyboardShortcut("Mod+U")}
160
179
  icon={RiUnderline}
161
180
  />
162
- <SimpleToolbarButton
163
- onClick={() => props.editor.chain().focus().toggleStrike().run()}
164
- isDisabled={props.editor.isActive("strike")}
181
+ <ToolbarButton
182
+ onClick={() => {
183
+ props.editor.view.focus();
184
+ props.editor.commands.toggleStrike();
185
+ }}
186
+ isSelected={props.editor.isActive("strike")}
165
187
  mainTooltip="Strike-through"
166
188
  secondaryTooltip={formatKeyboardShortcut("Mod+Shift+X")}
167
189
  icon={RiStrikethrough}
168
190
  />
169
- <SimpleToolbarButton
170
- onClick={() => props.editor.chain().focus().sinkListItem("block").run()}
191
+ <ToolbarButton
192
+ onClick={() => {
193
+ props.editor.view.focus();
194
+ props.editor.commands.sinkListItem("block");
195
+ }}
171
196
  isDisabled={!props.editor.can().sinkListItem("block")}
172
197
  mainTooltip="Indent"
173
198
  secondaryTooltip={formatKeyboardShortcut("Tab")}
174
199
  icon={RiIndentIncrease}
175
200
  />
176
201
 
177
- <SimpleToolbarButton
178
- onClick={() => props.editor.chain().focus().liftListItem("block").run()}
202
+ <ToolbarButton
203
+ onClick={() => {
204
+ props.editor.view.focus();
205
+ props.editor.commands.liftListItem("block");
206
+ }}
179
207
  isDisabled={
180
208
  !props.editor.can().command(({ state }) => {
181
209
  const block = findBlock(state.selection);
@@ -200,13 +228,13 @@ export const BubbleMenu = (props: { editor: Editor }) => {
200
228
  editor={props.editor}
201
229
  />
202
230
  {/* <SimpleBubbleMenuButton
203
- editor={props.editor}
204
- onClick={() => {
205
- const comment = this.props.commentStore.createComment();
206
- props.editor.chain().focus().setComment(comment.id).run();
207
- }}
208
- styleDetails={comment}
209
- /> */}
231
+ editor={props.editor}
232
+ onClick={() => {
233
+ const comment = this.props.commentStore.createComment();
234
+ props.editor.chain().focus().setComment(comment.id).run();
235
+ }}
236
+ styleDetails={comment}
237
+ /> */}
210
238
  </Toolbar>
211
239
  );
212
240
  };
@@ -2,12 +2,12 @@ import Tippy from "@tippyjs/react";
2
2
  import { Editor } from "@tiptap/core";
3
3
  import { useCallback, useState } from "react";
4
4
  import {
5
- SimpleToolbarButton,
6
- SimpleToolbarButtonProps,
7
- } from "../../../shared/components/toolbar/SimpleToolbarButton";
8
- import { HyperlinkEditMenu } from "../../Hyperlinks/menus/HyperlinkEditMenu";
5
+ ToolbarButton,
6
+ ToolbarButtonProps,
7
+ } from "../../../shared/components/toolbar/ToolbarButton";
8
+ import { EditHyperlinkMenu } from "../../Hyperlinks/menus/EditHyperlinkMenu";
9
9
 
10
- type Props = SimpleToolbarButtonProps & {
10
+ type Props = ToolbarButtonProps & {
11
11
  editor: Editor;
12
12
  };
13
13
 
@@ -41,11 +41,11 @@ export const LinkToolbarButton = (props: Props) => {
41
41
  : "";
42
42
 
43
43
  setCreationMenu(
44
- <HyperlinkEditMenu
44
+ <EditHyperlinkMenu
45
45
  key={Math.random() + ""} // Math.random to prevent old element from being re-used
46
46
  url={activeUrl}
47
47
  text={selectedText}
48
- onSubmit={onSubmit}
48
+ update={onSubmit}
49
49
  />
50
50
  );
51
51
  }, [props.editor]);
@@ -59,7 +59,7 @@ export const LinkToolbarButton = (props: Props) => {
59
59
  }}
60
60
  interactive={true}
61
61
  maxWidth={500}>
62
- <SimpleToolbarButton {...props} />
62
+ <ToolbarButton {...props} />
63
63
  </Tippy>
64
64
  );
65
65
  };
@@ -1,13 +1,19 @@
1
- import { NodeSelection, Plugin, PluginKey } from "prosemirror-state";
1
+ import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state";
2
+ import { Node } from "prosemirror-model";
2
3
  import * as pv from "prosemirror-view";
3
4
  import { EditorView } from "prosemirror-view";
4
5
  import ReactDOM from "react-dom";
5
6
  import { DragHandle } from "./components/DragHandle";
7
+ import { MantineProvider } from "@mantine/core";
8
+ import { BlockNoteTheme } from "../../BlockNoteTheme";
9
+ import { MultipleNodeSelection } from "../Blocks/MultipleNodeSelection";
6
10
 
7
11
  const serializeForClipboard = (pv as any).__serializeForClipboard;
8
12
  // code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799
9
13
 
10
14
  let horizontalAnchor: number;
15
+ let dragImageElement: Element | undefined;
16
+
11
17
  function getHorizontalAnchor() {
12
18
  if (!horizontalAnchor) {
13
19
  const firstBlockGroup = document.querySelector(
@@ -38,24 +44,6 @@ export function absoluteRect(element: HTMLElement) {
38
44
  return createRect(element.getBoundingClientRect());
39
45
  }
40
46
 
41
- function blockPosAtCoords(
42
- coords: { left: number; top: number },
43
- view: EditorView
44
- ) {
45
- let block = getDraggableBlockFromCoords(coords, view);
46
-
47
- if (block && block.node.nodeType === 1) {
48
- // TODO: this uses undocumented PM APIs? do we need this / let's add docs?
49
- const docView = (view as any).docView;
50
- let desc = docView.nearestDesc(block.node, true);
51
- if (!desc || desc === docView) {
52
- return null;
53
- }
54
- return desc.posBefore;
55
- }
56
- return null;
57
- }
58
-
59
47
  function getDraggableBlockFromCoords(
60
48
  coords: { left: number; top: number },
61
49
  view: EditorView
@@ -85,6 +73,107 @@ function getDraggableBlockFromCoords(
85
73
  return { node, id: node.getAttribute("data-id")! };
86
74
  }
87
75
 
76
+ function blockPositionFromCoords(
77
+ coords: { left: number; top: number },
78
+ view: EditorView
79
+ ) {
80
+ let block = getDraggableBlockFromCoords(coords, view);
81
+
82
+ if (block && block.node.nodeType === 1) {
83
+ // TODO: this uses undocumented PM APIs? do we need this / let's add docs?
84
+ const docView = (view as any).docView;
85
+ let desc = docView.nearestDesc(block.node, true);
86
+ if (!desc || desc === docView) {
87
+ return null;
88
+ }
89
+ return desc.posBefore;
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function blockPositionsFromSelection(
95
+ selection: Selection,
96
+ doc: Node
97
+ ) {
98
+ // Absolute positions just before the first block spanned by the selection, and just after the last block. Having the
99
+ // selection start and end just before and just after the target blocks ensures no whitespace/line breaks are left
100
+ // behind after dragging & dropping them.
101
+ let beforeFirstBlockPos: number;
102
+ let afterLastBlockPos: number;
103
+
104
+ // Even the user starts dragging blocks but drops them in the same place, the selection will still be moved just
105
+ // before & just after the blocks spanned by the selection, and therefore doesn't need to change if they try to drag
106
+ // the same blocks again. If this happens, the anchor & head move out of the block content node they were originally
107
+ // in. If the anchor should update but the head shouldn't and vice versa, it means the user selection is outside a
108
+ // block content node, which should never happen.
109
+ const selectionStartInBlockContent =
110
+ doc.resolve(selection.from).node().type.name === "content";
111
+ const selectionEndInBlockContent =
112
+ doc.resolve(selection.to).node().type.name === "content";
113
+
114
+ // Ensures that entire outermost nodes are selected if the selection spans multiple nesting levels.
115
+ const minDepth = Math.min(selection.$anchor.depth, selection.$head.depth);
116
+
117
+ if (selectionStartInBlockContent && selectionEndInBlockContent) {
118
+ // Absolute positions at the start of the first block in the selection and at the end of the last block. User
119
+ // selections will always start and end in block content nodes, but we want the start and end positions of their
120
+ // parent block nodes, which is why minDepth - 1 is used.
121
+ const startFirstBlockPos = selection.$from.start(minDepth - 1);
122
+ const endLastBlockPos = selection.$to.end(minDepth - 1);
123
+
124
+ // Shifting start and end positions by one moves them just outside the first and last selected blocks.
125
+ beforeFirstBlockPos = doc.resolve(startFirstBlockPos - 1).pos;
126
+ afterLastBlockPos = doc.resolve(endLastBlockPos + 1).pos;
127
+ } else {
128
+ beforeFirstBlockPos = selection.from;
129
+ afterLastBlockPos = selection.to;
130
+ }
131
+
132
+ return { from: beforeFirstBlockPos, to: afterLastBlockPos };
133
+ }
134
+
135
+ function setDragImage(view: EditorView, from: number, to = from) {
136
+ if (from === to) {
137
+ // Moves to position to be just after the first (and only) selected block.
138
+ to += view.state.doc.resolve(from + 1).node().nodeSize;
139
+ }
140
+
141
+ // Parent element is cloned to remove all unselected children without affecting the editor content.
142
+ const parentClone = view.domAtPos(from).node.cloneNode(true) as Element;
143
+ const parent = view.domAtPos(from).node as Element;
144
+
145
+ const getElementIndex = (parentElement: Element, targetElement: Element) =>
146
+ Array.prototype.indexOf.call(parentElement.children, targetElement);
147
+
148
+ const firstSelectedBlockIndex = getElementIndex(
149
+ parent,
150
+ // Expects from position to be just before the first selected block.
151
+ view.domAtPos(from + 1).node.parentElement!
152
+ );
153
+ const lastSelectedBlockIndex = getElementIndex(
154
+ parent,
155
+ // Expects to position to be just after the last selected block.
156
+ view.domAtPos(to - 1).node.parentElement!
157
+ );
158
+
159
+ for (let i = parent.childElementCount - 1; i >= 0; i--) {
160
+ if (i > lastSelectedBlockIndex || i < firstSelectedBlockIndex) {
161
+ parentClone.removeChild(parentClone.children[i]);
162
+ }
163
+ }
164
+
165
+ // dataTransfer.setDragImage(element) only works if element is attached to the DOM.
166
+ dragImageElement = parentClone;
167
+ document.body.appendChild(dragImageElement);
168
+ }
169
+
170
+ function unsetDragImage() {
171
+ if (dragImageElement !== undefined) {
172
+ document.body.removeChild(dragImageElement);
173
+ dragImageElement = undefined;
174
+ }
175
+ }
176
+
88
177
  function dragStart(e: DragEvent, view: EditorView) {
89
178
  if (!e.dataTransfer) {
90
179
  return;
@@ -94,11 +183,30 @@ function dragStart(e: DragEvent, view: EditorView) {
94
183
  left: view.dom.clientWidth / 2, // take middle of editor
95
184
  top: e.clientY,
96
185
  };
97
- let pos = blockPosAtCoords(coords, view);
186
+
187
+ let pos = blockPositionFromCoords(coords, view);
98
188
  if (pos != null) {
99
- view.dispatch(
100
- view.state.tr.setSelection(NodeSelection.create(view.state.doc, pos))
101
- );
189
+ const selection = view.state.selection;
190
+ const doc = view.state.doc;
191
+
192
+ const { from, to } = blockPositionsFromSelection(selection, doc);
193
+
194
+ const draggedBlockInSelection = from <= pos && pos < to;
195
+ const multipleBlocksSelected = !selection.$anchor
196
+ .node()
197
+ .eq(selection.$head.node());
198
+
199
+ if (draggedBlockInSelection && multipleBlocksSelected) {
200
+ view.dispatch(
201
+ view.state.tr.setSelection(MultipleNodeSelection.create(doc, from, to))
202
+ );
203
+ setDragImage(view, from, to);
204
+ } else {
205
+ view.dispatch(
206
+ view.state.tr.setSelection(NodeSelection.create(view.state.doc, pos))
207
+ );
208
+ setDragImage(view, pos);
209
+ }
102
210
 
103
211
  let slice = view.state.selection.content();
104
212
  let { dom, text } = serializeForClipboard(view, slice);
@@ -107,8 +215,7 @@ function dragStart(e: DragEvent, view: EditorView) {
107
215
  e.dataTransfer.setData("text/html", dom.innerHTML);
108
216
  e.dataTransfer.setData("text/plain", text);
109
217
  e.dataTransfer.effectAllowed = "move";
110
- const block = getDraggableBlockFromCoords(coords, view);
111
- e.dataTransfer.setDragImage(block?.node as any, 0, 0);
218
+ e.dataTransfer.setDragImage(dragImageElement!, 0, 0);
112
219
  view.dragging = { slice, move: true };
113
220
  }
114
221
  }
@@ -147,6 +254,7 @@ export const createDraggableBlocksPlugin = () => {
147
254
  dropElement.addEventListener("dragstart", (e) =>
148
255
  dragStart(e, editorView)
149
256
  );
257
+ dropElement.addEventListener("dragend", () => unsetDragImage());
150
258
 
151
259
  return {
152
260
  // update(view, prevState) {},
@@ -248,14 +356,16 @@ export const createDraggableBlocksPlugin = () => {
248
356
  dropElement.style.top = rect.top + "px";
249
357
 
250
358
  ReactDOM.render(
251
- <DragHandle
252
- onShow={onShow}
253
- onHide={onHide}
254
- onAddClicked={onAddClicked}
255
- key={block.id + ""}
256
- view={view}
257
- coords={coords}
258
- />,
359
+ <MantineProvider theme={BlockNoteTheme}>
360
+ <DragHandle
361
+ onShow={onShow}
362
+ onHide={onHide}
363
+ onAddClicked={onAddClicked}
364
+ key={block.id + ""}
365
+ view={view}
366
+ coords={coords}
367
+ />
368
+ </MantineProvider>,
259
369
  dropElement
260
370
  );
261
371
  return true;