@blocknote/core 0.22.0 → 0.23.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 (67) hide show
  1. package/dist/blocknote.js +2315 -1711
  2. package/dist/blocknote.js.map +1 -1
  3. package/dist/blocknote.umd.cjs +7 -7
  4. package/dist/blocknote.umd.cjs.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/dist/tsconfig.tsbuildinfo +1 -1
  7. package/dist/webpack-stats.json +1 -1
  8. package/package.json +2 -2
  9. package/src/api/clipboard/__snapshots__/internal/basicBlocks.html +1 -0
  10. package/src/api/clipboard/__snapshots__/internal/basicBlocksWithProps.html +1 -0
  11. package/src/api/clipboard/clipboardInternal.test.ts +126 -0
  12. package/src/api/exporters/html/__snapshots__/pageBreak/basic/external.html +1 -0
  13. package/src/api/exporters/html/__snapshots__/pageBreak/basic/internal.html +1 -0
  14. package/src/api/exporters/markdown/__snapshots__/pageBreak/basic/markdown.md +0 -0
  15. package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +16 -0
  16. package/src/api/parsers/html/__snapshots__/parse-codeblocks.json +62 -0
  17. package/src/api/parsers/html/parseHTML.test.ts +9 -0
  18. package/src/api/testUtil/cases/defaultSchema.ts +15 -1
  19. package/src/blocks/CodeBlockContent/CodeBlockContent.ts +32 -11
  20. package/src/blocks/HeadingBlockContent/HeadingBlockContent.ts +0 -9
  21. package/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +1 -1
  22. package/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts +1 -1
  23. package/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +1 -1
  24. package/src/blocks/PageBreakBlockContent/PageBreakBlockContent.ts +49 -0
  25. package/src/blocks/PageBreakBlockContent/getPageBreakSlashMenuItems.ts +45 -0
  26. package/src/blocks/PageBreakBlockContent/schema.ts +40 -0
  27. package/src/editor/Block.css +15 -1
  28. package/src/editor/BlockNoteEditor.ts +17 -0
  29. package/src/editor/BlockNoteExtensions.ts +111 -16
  30. package/src/editor/editor.css +22 -7
  31. package/src/extensions/SideMenu/SideMenuPlugin.ts +115 -23
  32. package/src/extensions/SideMenu/dragging.ts +0 -1
  33. package/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts +1 -1
  34. package/src/i18n/locales/ar.ts +6 -0
  35. package/src/i18n/locales/de.ts +6 -0
  36. package/src/i18n/locales/en.ts +6 -0
  37. package/src/i18n/locales/es.ts +6 -0
  38. package/src/i18n/locales/fr.ts +47 -17
  39. package/src/i18n/locales/hr.ts +72 -54
  40. package/src/i18n/locales/index.ts +1 -0
  41. package/src/i18n/locales/is.ts +6 -0
  42. package/src/i18n/locales/it.ts +315 -0
  43. package/src/i18n/locales/ja.ts +6 -0
  44. package/src/i18n/locales/ko.ts +6 -0
  45. package/src/i18n/locales/nl.ts +6 -0
  46. package/src/i18n/locales/pl.ts +6 -0
  47. package/src/i18n/locales/pt.ts +6 -0
  48. package/src/i18n/locales/ru.ts +6 -0
  49. package/src/i18n/locales/vi.ts +6 -0
  50. package/src/i18n/locales/zh.ts +6 -0
  51. package/src/index.ts +3 -0
  52. package/types/src/api/testUtil/cases/defaultSchema.d.ts +2 -1
  53. package/types/src/blocks/CodeBlockContent/CodeBlockContent.d.ts +2 -0
  54. package/types/src/blocks/PageBreakBlockContent/PageBreakBlockContent.d.ts +31 -0
  55. package/types/src/blocks/PageBreakBlockContent/getPageBreakSlashMenuItems.d.ts +8 -0
  56. package/types/src/blocks/PageBreakBlockContent/schema.d.ts +86 -0
  57. package/types/src/editor/BlockNoteEditor.d.ts +15 -0
  58. package/types/src/editor/BlockNoteExtensions.d.ts +2 -0
  59. package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +25 -5
  60. package/types/src/extensions/SuggestionMenu/DefaultSuggestionItem.d.ts +1 -1
  61. package/types/src/i18n/locales/de.d.ts +6 -0
  62. package/types/src/i18n/locales/en.d.ts +6 -0
  63. package/types/src/i18n/locales/es.d.ts +6 -0
  64. package/types/src/i18n/locales/hr.d.ts +6 -0
  65. package/types/src/i18n/locales/index.d.ts +1 -0
  66. package/types/src/i18n/locales/it.d.ts +245 -0
  67. package/types/src/index.d.ts +3 -0
@@ -0,0 +1,40 @@
1
+ import { BlockNoteSchema } from "../../editor/BlockNoteSchema.js";
2
+ import {
3
+ BlockSchema,
4
+ InlineContentSchema,
5
+ StyleSchema,
6
+ } from "../../schema/index.js";
7
+ import { PageBreak } from "./PageBreakBlockContent.js";
8
+
9
+ export const pageBreakSchema = BlockNoteSchema.create({
10
+ blockSpecs: {
11
+ pageBreak: PageBreak,
12
+ },
13
+ });
14
+
15
+ /**
16
+ * Adds page break support to the given schema.
17
+ */
18
+ export const withPageBreak = <
19
+ B extends BlockSchema,
20
+ I extends InlineContentSchema,
21
+ S extends StyleSchema
22
+ >(
23
+ schema: BlockNoteSchema<B, I, S>
24
+ ) => {
25
+ return BlockNoteSchema.create({
26
+ blockSpecs: {
27
+ ...schema.blockSpecs,
28
+ ...pageBreakSchema.blockSpecs,
29
+ },
30
+ inlineContentSpecs: schema.inlineContentSpecs,
31
+ styleSpecs: schema.styleSpecs,
32
+ }) as any as BlockNoteSchema<
33
+ // typescript needs some help here
34
+ B & {
35
+ pageBreak: typeof PageBreak.config;
36
+ },
37
+ I,
38
+ S
39
+ >;
40
+ };
@@ -308,6 +308,20 @@ NESTED BLOCKS
308
308
  transition-delay: 0.1s;
309
309
  }
310
310
 
311
+ /* PAGE BREAK */
312
+ .bn-block-content[data-content-type="pageBreak"] > div {
313
+ width: 100%;
314
+ height: 0;
315
+ border-top: dotted rgb(125, 121, 122) 2px;
316
+ margin-block: 11px;
317
+ }
318
+
319
+ @media print {
320
+ .bn-block-content[data-content-type="pageBreak"] > div {
321
+ page-break-after: always;
322
+ }
323
+ }
324
+
311
325
  /* FILES */
312
326
 
313
327
  /* Element that wraps content for all file blocks */
@@ -336,7 +350,7 @@ NESTED BLOCKS
336
350
 
337
351
  .bn-editor[contenteditable="true"] [data-file-block] .bn-add-file-button:hover,
338
352
  [data-file-block] .bn-file-name-with-icon:hover,
339
- .ProseMirror-selectednode .bn-file-name-with-icon{
353
+ .ProseMirror-selectednode .bn-file-name-with-icon {
340
354
  background-color: rgb(225, 225, 225);
341
355
  }
342
356
 
@@ -196,6 +196,13 @@ export type BlockNoteEditorOptions<
196
196
  * Optional function to customize how cursors of users are rendered
197
197
  */
198
198
  renderCursor?: (user: any) => HTMLElement;
199
+ /**
200
+ * Optional flag to set when the user label should be shown with the default
201
+ * collaboration cursor. Setting to "always" will always show the label,
202
+ * while "activity" will only show the label when the user moves the cursor
203
+ * or types. Defaults to "activity".
204
+ */
205
+ showCursorLabels?: "always" | "activity";
199
206
  };
200
207
 
201
208
  /**
@@ -245,6 +252,15 @@ export type BlockNoteEditorOptions<
245
252
  @default "prefer-navigate-ui"
246
253
  */
247
254
  tabBehavior: "prefer-navigate-ui" | "prefer-indent";
255
+
256
+ /**
257
+ * The detection mode for showing the side menu - "viewport" always shows the
258
+ * side menu for the block next to the mouse cursor, while "editor" only shows
259
+ * it when hovering the editor or the side menu itself.
260
+ *
261
+ * @default "viewport"
262
+ */
263
+ sideMenuDetection: "viewport" | "editor";
248
264
  };
249
265
 
250
266
  const blockNoteTipTapOptions = {
@@ -423,6 +439,7 @@ export class BlockNoteEditor<
423
439
  dropCursor: this.options.dropCursor ?? dropCursor,
424
440
  placeholders: newOptions.placeholders,
425
441
  tabBehavior: newOptions.tabBehavior,
442
+ sideMenuDetection: newOptions.sideMenuDetection || "viewport",
426
443
  });
427
444
 
428
445
  // add extensions from _tiptapOptions
@@ -1,4 +1,5 @@
1
1
  import { AnyExtension, Extension, extensions } from "@tiptap/core";
2
+ import { Awareness } from "y-protocols/awareness";
2
3
 
3
4
  import type { BlockNoteEditor, BlockNoteExtension } from "./BlockNoteEditor.js";
4
5
 
@@ -64,6 +65,7 @@ type ExtensionOptions<
64
65
  };
65
66
  provider: any;
66
67
  renderCursor?: (user: any) => HTMLElement;
68
+ showCursorLabels?: "always" | "activity";
67
69
  };
68
70
  disableExtensions: string[] | undefined;
69
71
  setIdAttribute?: boolean;
@@ -72,6 +74,7 @@ type ExtensionOptions<
72
74
  dropCursor: (opts: any) => Plugin;
73
75
  placeholders: Record<string | "default", string>;
74
76
  tabBehavior?: "prefer-navigate-ui" | "prefer-indent";
77
+ sideMenuDetection: "viewport" | "editor";
75
78
  };
76
79
 
77
80
  /**
@@ -97,7 +100,10 @@ export const getBlockNoteExtensions = <
97
100
  opts.editor
98
101
  );
99
102
  ret["linkToolbar"] = new LinkToolbarProsemirrorPlugin(opts.editor);
100
- ret["sideMenu"] = new SideMenuProsemirrorPlugin(opts.editor);
103
+ ret["sideMenu"] = new SideMenuProsemirrorPlugin(
104
+ opts.editor,
105
+ opts.sideMenuDetection
106
+ );
101
107
  ret["suggestionMenus"] = new SuggestionMenuProseMirrorPlugin(opts.editor);
102
108
  ret["filePanel"] = new FilePanelProsemirrorPlugin(opts.editor as any);
103
109
  ret["placeholder"] = new PlaceholderPlugin(opts.editor, opts.placeholders);
@@ -246,25 +252,114 @@ const getTipTapExtensions = <
246
252
  fragment: opts.collaboration.fragment,
247
253
  })
248
254
  );
249
- if (opts.collaboration.provider?.awareness) {
250
- const defaultRender = (user: { color: string; name: string }) => {
251
- const cursor = document.createElement("span");
252
255
 
253
- cursor.classList.add("collaboration-cursor__caret");
254
- cursor.setAttribute("style", `border-color: ${user.color}`);
256
+ const awareness = opts.collaboration?.provider.awareness as Awareness;
257
+
258
+ if (awareness) {
259
+ const cursors = new Map<
260
+ number,
261
+ { element: HTMLElement; hideTimeout: NodeJS.Timeout | undefined }
262
+ >();
263
+
264
+ if (opts.collaboration.showCursorLabels !== "always") {
265
+ awareness.on(
266
+ "change",
267
+ ({
268
+ updated,
269
+ }: {
270
+ added: Array<number>;
271
+ updated: Array<number>;
272
+ removed: Array<number>;
273
+ }) => {
274
+ for (const clientID of updated) {
275
+ const cursor = cursors.get(clientID);
276
+
277
+ if (cursor) {
278
+ cursor.element.setAttribute("data-active", "");
279
+
280
+ if (cursor.hideTimeout) {
281
+ clearTimeout(cursor.hideTimeout);
282
+ }
283
+
284
+ cursors.set(clientID, {
285
+ element: cursor.element,
286
+ hideTimeout: setTimeout(() => {
287
+ cursor.element.removeAttribute("data-active");
288
+ }, 2000),
289
+ });
290
+ }
291
+ }
292
+ }
293
+ );
294
+ }
295
+
296
+ const createCursor = (clientID: number, name: string, color: string) => {
297
+ const cursorElement = document.createElement("span");
298
+
299
+ cursorElement.classList.add("collaboration-cursor__caret");
300
+ cursorElement.setAttribute("style", `border-color: ${color}`);
301
+ if (opts.collaboration?.showCursorLabels === "always") {
302
+ cursorElement.setAttribute("data-active", "");
303
+ }
304
+
305
+ const labelElement = document.createElement("span");
306
+
307
+ labelElement.classList.add("collaboration-cursor__label");
308
+ labelElement.setAttribute("style", `background-color: ${color}`);
309
+ labelElement.insertBefore(document.createTextNode(name), null);
310
+
311
+ cursorElement.insertBefore(document.createTextNode("\u2060"), null); // Non-breaking space
312
+ cursorElement.insertBefore(labelElement, null);
313
+ cursorElement.insertBefore(document.createTextNode("\u2060"), null); // Non-breaking space
314
+
315
+ cursors.set(clientID, {
316
+ element: cursorElement,
317
+ hideTimeout: undefined,
318
+ });
319
+
320
+ if (opts.collaboration?.showCursorLabels !== "always") {
321
+ cursorElement.addEventListener("mouseenter", () => {
322
+ const cursor = cursors.get(clientID)!;
323
+ cursor.element.setAttribute("data-active", "");
324
+
325
+ if (cursor.hideTimeout) {
326
+ clearTimeout(cursor.hideTimeout);
327
+ cursors.set(clientID, {
328
+ element: cursor.element,
329
+ hideTimeout: undefined,
330
+ });
331
+ }
332
+ });
333
+
334
+ cursorElement.addEventListener("mouseleave", () => {
335
+ const cursor = cursors.get(clientID)!;
336
+
337
+ cursors.set(clientID, {
338
+ element: cursor.element,
339
+ hideTimeout: setTimeout(() => {
340
+ cursor.element.removeAttribute("data-active");
341
+ }, 2000),
342
+ });
343
+ });
344
+ }
345
+
346
+ return cursors.get(clientID)!;
347
+ };
348
+
349
+ const defaultRender = (user: { color: string; name: string }) => {
350
+ const clientState = [...awareness.getStates().entries()].find(
351
+ (state) => state[1].user === user
352
+ );
255
353
 
256
- const label = document.createElement("span");
354
+ if (!clientState) {
355
+ throw new Error("Could not find client state for user");
356
+ }
257
357
 
258
- label.classList.add("collaboration-cursor__label");
259
- label.setAttribute("style", `background-color: ${user.color}`);
260
- label.insertBefore(document.createTextNode(user.name), null);
358
+ const clientID = clientState[0];
261
359
 
262
- const nonbreakingSpace1 = document.createTextNode("\u2060");
263
- const nonbreakingSpace2 = document.createTextNode("\u2060");
264
- cursor.insertBefore(nonbreakingSpace1, null);
265
- cursor.insertBefore(label, null);
266
- cursor.insertBefore(nonbreakingSpace2, null);
267
- return cursor;
360
+ return (
361
+ cursors.get(clientID) || createCursor(clientID, user.name, user.color)
362
+ ).element;
268
363
  };
269
364
  tiptapExtensions.push(
270
365
  CollaborationCursor.configure({
@@ -83,7 +83,6 @@ Tippy popups that are appended to document.body directly
83
83
  border-right: 1px solid #0d0d0d;
84
84
  margin-left: -1px;
85
85
  margin-right: -1px;
86
- pointer-events: none;
87
86
  position: relative;
88
87
  word-break: normal;
89
88
  white-space: nowrap !important;
@@ -92,17 +91,33 @@ Tippy popups that are appended to document.body directly
92
91
  /* Render the username above the caret */
93
92
  .collaboration-cursor__label {
94
93
  border-radius: 3px 3px 3px 0;
95
- color: #0d0d0d;
96
94
  font-size: 12px;
97
95
  font-style: normal;
98
96
  font-weight: 600;
99
- left: -1px;
100
97
  line-height: normal;
101
- padding: 0.1rem 0.3rem;
98
+ left: -1px;
99
+ overflow: hidden;
102
100
  position: absolute;
103
- top: -1.4em;
104
- user-select: none;
105
101
  white-space: nowrap;
102
+
103
+ color: transparent;
104
+ max-height: 4px;
105
+ max-width: 4px;
106
+ padding: 0;
107
+ top: 0;
108
+
109
+ transition: all 0.2s;
110
+
111
+ }
112
+
113
+ .collaboration-cursor__caret[data-active] > .collaboration-cursor__label {
114
+ color: #0d0d0d;
115
+ max-height: 1.1rem;
116
+ max-width: 20rem;
117
+ padding: 0.1rem 0.3rem;
118
+ top: -14px;
119
+
120
+ transition: all 0.2s;
106
121
  }
107
122
 
108
123
  /* .tableWrapper {
@@ -134,7 +149,7 @@ Tippy popups that are appended to document.body directly
134
149
  .bn-editor [data-content-type="table"] th,
135
150
  .bn-editor [data-content-type="table"] td {
136
151
  border: 1px solid #ddd;
137
- padding: 3px 5px;
152
+ padding: 5px 10px;
138
153
  }
139
154
 
140
155
  .bn-editor [data-content-type="table"] th {
@@ -1,6 +1,6 @@
1
- import { PluginView } from "@tiptap/pm/state";
2
- import { EditorState, Plugin, PluginKey } from "prosemirror-state";
3
- import { EditorView } from "prosemirror-view";
1
+ import { DOMParser, Slice } from "@tiptap/pm/model";
2
+ import { EditorState, Plugin, PluginKey, PluginView } from "@tiptap/pm/state";
3
+ import { EditorView } from "@tiptap/pm/view";
4
4
 
5
5
  import { Block } from "../../blocks/defaultBlocks.js";
6
6
  import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
@@ -14,6 +14,7 @@ import { EventEmitter } from "../../util/EventEmitter.js";
14
14
  import { initializeESMDependencies } from "../../util/esmDependencies.js";
15
15
  import { getDraggableBlockFromElement } from "../getDraggableBlockFromElement.js";
16
16
  import { dragStart, unsetDragImage } from "./dragging.js";
17
+
17
18
  export type SideMenuState<
18
19
  BSchema extends BlockSchema,
19
20
  I extends InlineContentSchema,
@@ -28,9 +29,14 @@ const PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP = 0.1;
28
29
  function getBlockFromCoords(
29
30
  view: EditorView,
30
31
  coords: { left: number; top: number },
32
+ sideMenuDetection: "viewport" | "editor",
31
33
  adjustForColumns = true
32
34
  ) {
33
- const elements = view.root.elementsFromPoint(coords.left, coords.top);
35
+ const elements = view.root.elementsFromPoint(
36
+ // bit hacky - offset x position to right to account for the width of sidemenu itself
37
+ coords.left + (sideMenuDetection === "editor" ? 50 : 0),
38
+ coords.top
39
+ );
34
40
 
35
41
  for (const element of elements) {
36
42
  if (!view.dom.contains(element)) {
@@ -46,6 +52,7 @@ function getBlockFromCoords(
46
52
  left: coords.left + 50, // bit hacky, but if we're inside a column, offset x position to right to account for the width of sidemenu itself
47
53
  top: coords.top,
48
54
  },
55
+ sideMenuDetection,
49
56
  false
50
57
  );
51
58
  }
@@ -60,7 +67,8 @@ function getBlockFromMousePos(
60
67
  x: number;
61
68
  y: number;
62
69
  },
63
- view: EditorView
70
+ view: EditorView,
71
+ sideMenuDetection: "viewport" | "editor"
64
72
  ): { node: HTMLElement; id: string } | undefined {
65
73
  // Editor itself may have padding or other styling which affects
66
74
  // size/position, so we get the boundingRect of the first child (i.e. the
@@ -76,7 +84,7 @@ function getBlockFromMousePos(
76
84
 
77
85
  // this.horizontalPosAnchor = editorBoundingBox.x;
78
86
 
79
- // Gets block at mouse cursor's vertical position.
87
+ // Gets block at mouse cursor's position.
80
88
  const coords = {
81
89
  left: mousePos.x,
82
90
  top: mousePos.y,
@@ -85,15 +93,18 @@ function getBlockFromMousePos(
85
93
  const mouseLeftOfEditor = coords.left < editorBoundingBox.left;
86
94
  const mouseRightOfEditor = coords.left > editorBoundingBox.right;
87
95
 
88
- if (mouseLeftOfEditor) {
89
- coords.left = editorBoundingBox.left + 10;
90
- }
96
+ // Clamps the x position to the editor's bounding box.
97
+ if (sideMenuDetection === "viewport") {
98
+ if (mouseLeftOfEditor) {
99
+ coords.left = editorBoundingBox.left + 10;
100
+ }
91
101
 
92
- if (mouseRightOfEditor) {
93
- coords.left = editorBoundingBox.right - 10;
102
+ if (mouseRightOfEditor) {
103
+ coords.left = editorBoundingBox.right - 10;
104
+ }
94
105
  }
95
106
 
96
- let block = getBlockFromCoords(view, coords);
107
+ let block = getBlockFromCoords(view, coords, sideMenuDetection);
97
108
 
98
109
  if (!mouseRightOfEditor && block) {
99
110
  // note: this case is not necessary when we're on the right side of the editor
@@ -101,14 +112,14 @@ function getBlockFromMousePos(
101
112
  /* Now, because blocks can be nested
102
113
  | BlockA |
103
114
  x | BlockB y|
104
-
115
+
105
116
  hovering over position x (the "margin of block B") will return block A instead of block B.
106
117
  to fix this, we get the block from the right side of block A (position y, which will fall in BlockB correctly)
107
118
  */
108
119
 
109
120
  const rect = block.node.getBoundingClientRect();
110
121
  coords.left = rect.right - 10;
111
- block = getBlockFromCoords(view, coords, false);
122
+ block = getBlockFromCoords(view, coords, "viewport", false);
112
123
  }
113
124
 
114
125
  return block;
@@ -132,8 +143,11 @@ export class SideMenuView<
132
143
 
133
144
  public menuFrozen = false;
134
145
 
146
+ public isDragOrigin = false;
147
+
135
148
  constructor(
136
149
  private readonly editor: BlockNoteEditor<BSchema, I, S>,
150
+ private readonly sideMenuDetection: "viewport" | "editor",
137
151
  private readonly pmView: EditorView,
138
152
  emitUpdate: (state: SideMenuState<BSchema, I, S>) => void
139
153
  ) {
@@ -146,14 +160,18 @@ export class SideMenuView<
146
160
  };
147
161
 
148
162
  this.pmView.root.addEventListener(
149
- "drop",
150
- this.onDrop as EventListener,
151
- true
163
+ "dragstart",
164
+ this.onDragStart as EventListener
152
165
  );
153
166
  this.pmView.root.addEventListener(
154
167
  "dragover",
155
168
  this.onDragOver as EventListener
156
169
  );
170
+ this.pmView.root.addEventListener(
171
+ "drop",
172
+ this.onDrop as EventListener,
173
+ true
174
+ );
157
175
  initializeESMDependencies();
158
176
 
159
177
  // Shows or updates menu position whenever the cursor moves, if the menu isn't frozen.
@@ -181,7 +199,11 @@ export class SideMenuView<
181
199
  return;
182
200
  }
183
201
 
184
- const block = getBlockFromMousePos(this.mousePos, this.pmView);
202
+ const block = getBlockFromMousePos(
203
+ this.mousePos,
204
+ this.pmView,
205
+ this.sideMenuDetection
206
+ );
185
207
 
186
208
  // Closes the menu if the mouse cursor is beyond the editor vertically.
187
209
  if (!block || !this.editor.isEditable) {
@@ -249,7 +271,16 @@ export class SideMenuView<
249
271
  onDrop = (event: DragEvent) => {
250
272
  this.editor._tiptapEditor.commands.blur();
251
273
 
274
+ // ProseMirror doesn't remove the dragged content if it's dropped outside
275
+ // the editor (e.g. to other editors), so we need to do it manually. Since
276
+ // the dragged content is the same as the selected content, we can just
277
+ // delete the selection.
278
+ if (this.isDragOrigin && !this.pmView.dom.contains(event.target as Node)) {
279
+ this.pmView.dispatch(this.pmView.state.tr.deleteSelection());
280
+ }
281
+
252
282
  if (
283
+ this.sideMenuDetection === "editor" ||
253
284
  (event as any).synthetic ||
254
285
  !event.dataTransfer?.types.includes("blocknote/html")
255
286
  ) {
@@ -268,6 +299,46 @@ export class SideMenuView<
268
299
  }
269
300
  };
270
301
 
302
+ /**
303
+ * If a block is being dragged, ProseMirror usually gets the context of what's
304
+ * being dragged from `view.dragging`, which is automatically set when a
305
+ * `dragstart` event fires in the editor. However, if the user tries to drag
306
+ * and drop blocks between multiple editors, only the one in which the drag
307
+ * began has that context, so we need to set it on the others manually. This
308
+ * ensures that PM always drops the blocks in between other blocks, and not
309
+ * inside them.
310
+ *
311
+ * After the `dragstart` event fires on the drag handle, it sets
312
+ * `blocknote/html` data on the clipboard. This handler fires right after,
313
+ * parsing the `blocknote/html` data into nodes and setting them on
314
+ * `view.dragging`.
315
+ *
316
+ * Note: Setting `view.dragging` on `dragover` would be better as the user
317
+ * could then drag between editors in different windows, but you can only
318
+ * access `dataTransfer` contents on `dragstart` and `drop` events.
319
+ */
320
+ onDragStart = (event: DragEvent) => {
321
+ if (!this.pmView.dragging) {
322
+ const html = event.dataTransfer?.getData("blocknote/html");
323
+ if (!html) {
324
+ return;
325
+ }
326
+
327
+ const element = document.createElement("div");
328
+ element.innerHTML = html;
329
+
330
+ const parser = DOMParser.fromSchema(this.pmView.state.schema);
331
+ const node = parser.parse(element, {
332
+ topNode: this.pmView.state.schema.nodes["blockGroup"].create(),
333
+ });
334
+
335
+ this.pmView.dragging = {
336
+ slice: new Slice(node.content, 0, 0),
337
+ move: true,
338
+ };
339
+ }
340
+ };
341
+
271
342
  /**
272
343
  * If the event is outside the editor contents,
273
344
  * we dispatch a fake event, so that we can still drop the content
@@ -275,11 +346,13 @@ export class SideMenuView<
275
346
  */
276
347
  onDragOver = (event: DragEvent) => {
277
348
  if (
349
+ this.sideMenuDetection === "editor" ||
278
350
  (event as any).synthetic ||
279
351
  !event.dataTransfer?.types.includes("blocknote/html")
280
352
  ) {
281
353
  return;
282
354
  }
355
+
283
356
  const pos = this.pmView.posAtCoords({
284
357
  left: event.clientX,
285
358
  top: event.clientY,
@@ -424,11 +497,14 @@ export class SideMenuView<
424
497
  this.onMouseMove as EventListener,
425
498
  true
426
499
  );
500
+ this.pmView.root.removeEventListener(
501
+ "dragstart",
502
+ this.onDragStart as EventListener
503
+ );
427
504
  this.pmView.root.removeEventListener(
428
505
  "dragover",
429
506
  this.onDragOver as EventListener
430
507
  );
431
-
432
508
  this.pmView.root.removeEventListener(
433
509
  "drop",
434
510
  this.onDrop as EventListener,
@@ -452,14 +528,22 @@ export class SideMenuProsemirrorPlugin<
452
528
  public view: SideMenuView<BSchema, I, S> | undefined;
453
529
  public readonly plugin: Plugin;
454
530
 
455
- constructor(private readonly editor: BlockNoteEditor<BSchema, I, S>) {
531
+ constructor(
532
+ private readonly editor: BlockNoteEditor<BSchema, I, S>,
533
+ sideMenuDetection: "viewport" | "editor"
534
+ ) {
456
535
  super();
457
536
  this.plugin = new Plugin({
458
537
  key: sideMenuPluginKey,
459
538
  view: (editorView) => {
460
- this.view = new SideMenuView(editor, editorView, (state) => {
461
- this.emit("update", state);
462
- });
539
+ this.view = new SideMenuView(
540
+ editor,
541
+ sideMenuDetection,
542
+ editorView,
543
+ (state) => {
544
+ this.emit("update", state);
545
+ }
546
+ );
463
547
  return this.view;
464
548
  },
465
549
  });
@@ -479,6 +563,10 @@ export class SideMenuProsemirrorPlugin<
479
563
  },
480
564
  block: Block<BSchema, I, S>
481
565
  ) => {
566
+ if (this.view) {
567
+ this.view.isDragOrigin = true;
568
+ }
569
+
482
570
  dragStart(event, block, this.editor);
483
571
  };
484
572
 
@@ -489,6 +577,10 @@ export class SideMenuProsemirrorPlugin<
489
577
  if (this.editor.prosemirrorView) {
490
578
  unsetDragImage(this.editor.prosemirrorView.root);
491
579
  }
580
+
581
+ if (this.view) {
582
+ this.view.isDragOrigin = false;
583
+ }
492
584
  };
493
585
  /**
494
586
  * Freezes the side menu. When frozen, the side menu will stay
@@ -202,6 +202,5 @@ export function dragStart<
202
202
  e.dataTransfer.setData("text/plain", plainText);
203
203
  e.dataTransfer.effectAllowed = "move";
204
204
  e.dataTransfer.setDragImage(dragImageElement!, 0, 0);
205
- view.dragging = { slice: selectedSlice, move: true };
206
205
  }
207
206
  }
@@ -1,7 +1,7 @@
1
1
  import type { Dictionary } from "../../i18n/dictionary.js";
2
2
 
3
3
  export type DefaultSuggestionItem = {
4
- key: keyof Dictionary["slash_menu"];
4
+ key: keyof Omit<Dictionary["slash_menu"], "page_break">;
5
5
  title: string;
6
6
  onItemClick: () => void;
7
7
  subtext?: string;
@@ -58,6 +58,12 @@ export const ar: Dictionary = {
58
58
  aliases: ["كود", "مسبق"],
59
59
  group: "الكتل الأساسية",
60
60
  },
61
+ page_break: {
62
+ title: "فاصل الصفحة",
63
+ subtext: "فاصل الصفحة",
64
+ aliases: ["page", "break", "separator", "فاصل", "الصفحة"],
65
+ group: "الكتل الأساسية",
66
+ },
61
67
  table: {
62
68
  title: "جدول",
63
69
  subtext: "يستخدم للجداول",
@@ -56,6 +56,12 @@ export const de = {
56
56
  aliases: ["code", "pre"],
57
57
  group: "Grundlegende blöcke",
58
58
  },
59
+ page_break: {
60
+ title: "Seitenumbruch",
61
+ subtext: "Seitentrenner",
62
+ aliases: ["page", "break", "separator", "seitenumbruch", "trenner"],
63
+ group: "Grundlegende Blöcke",
64
+ },
59
65
  table: {
60
66
  title: "Tabelle",
61
67
  subtext: "Tabelle mit editierbaren Zellen",
@@ -56,6 +56,12 @@ export const en = {
56
56
  aliases: ["code", "pre"],
57
57
  group: "Basic blocks",
58
58
  },
59
+ page_break: {
60
+ title: "Page Break",
61
+ subtext: "Page separator",
62
+ aliases: ["page", "break", "separator"],
63
+ group: "Basic blocks",
64
+ },
59
65
  table: {
60
66
  title: "Table",
61
67
  subtext: "Table with editable cells",
@@ -55,6 +55,12 @@ export const es = {
55
55
  aliases: ["code", "pre"],
56
56
  group: "Bloques básicos",
57
57
  },
58
+ page_break: {
59
+ title: "Salto de página",
60
+ subtext: "Separador de página",
61
+ aliases: ["page", "break", "separator", "salto", "separador"],
62
+ group: "Bloques básicos",
63
+ },
58
64
  table: {
59
65
  title: "Tabla",
60
66
  subtext: "Tabla con celdas editables",