@blocknote/core 0.33.0 → 0.34.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 (39) hide show
  1. package/dist/blocknote.cjs +9 -9
  2. package/dist/blocknote.cjs.map +1 -1
  3. package/dist/blocknote.js +1838 -1715
  4. package/dist/blocknote.js.map +1 -1
  5. package/dist/tsconfig.tsbuildinfo +1 -1
  6. package/dist/webpack-stats.json +1 -1
  7. package/package.json +2 -2
  8. package/src/api/__snapshots__/blocks-indented-changed.json +129 -0
  9. package/src/api/__snapshots__/blocks-moved-deeper-into-nesting.json +164 -0
  10. package/src/api/__snapshots__/blocks-moved-multiple-in-same-transaction.json +188 -0
  11. package/src/api/__snapshots__/blocks-moved-to-different-parent.json +78 -0
  12. package/src/api/__snapshots__/blocks-moved-to-root-level.json +78 -0
  13. package/src/api/__snapshots__/blocks-outdented-changed.json +129 -0
  14. package/src/api/blockManipulation/commands/nestBlock/nestBlock.ts +58 -59
  15. package/src/api/nodeUtil.test.ts +228 -1
  16. package/src/api/nodeUtil.ts +135 -118
  17. package/src/api/parsers/markdown/detectMarkdown.test.ts +211 -0
  18. package/src/api/parsers/markdown/detectMarkdown.ts +3 -2
  19. package/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +2 -1
  20. package/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +2 -1
  21. package/src/blocks/ToggleWrapper/createToggleWrapper.ts +2 -0
  22. package/src/blocks/defaultBlockTypeGuards.ts +30 -0
  23. package/src/editor/BlockNoteEditor.ts +27 -10
  24. package/src/editor/BlockNoteExtensions.ts +3 -5
  25. package/src/exporter/Exporter.ts +2 -0
  26. package/src/exporter/mapping.ts +1 -0
  27. package/src/extensions/BlockChange/BlockChangePlugin.ts +66 -0
  28. package/src/extensions/SideMenu/SideMenuPlugin.ts +290 -207
  29. package/src/schema/inlineContent/types.ts +8 -0
  30. package/types/src/api/nodeUtil.d.ts +15 -21
  31. package/types/src/api/parsers/markdown/detectMarkdown.test.d.ts +1 -0
  32. package/types/src/blocks/defaultBlockTypeGuards.d.ts +7 -1
  33. package/types/src/editor/BlockNoteEditor.d.ts +11 -8
  34. package/types/src/editor/BlockNoteExtensions.d.ts +0 -1
  35. package/types/src/exporter/Exporter.d.ts +1 -1
  36. package/types/src/exporter/mapping.d.ts +1 -1
  37. package/types/src/extensions/BlockChange/BlockChangePlugin.d.ts +15 -0
  38. package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +50 -9
  39. package/types/src/schema/inlineContent/types.d.ts +4 -0
@@ -30,19 +30,14 @@ export type SideMenuState<
30
30
  block: Block<BSchema, I, S>;
31
31
  };
32
32
 
33
- const PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP = 0.1;
33
+ const DISTANCE_TO_CONSIDER_EDITOR_BOUNDS = 250;
34
34
 
35
35
  function getBlockFromCoords(
36
36
  view: EditorView,
37
37
  coords: { left: number; top: number },
38
- sideMenuDetection: "viewport" | "editor",
39
38
  adjustForColumns = true,
40
39
  ) {
41
- const elements = view.root.elementsFromPoint(
42
- // bit hacky - offset x position to right to account for the width of sidemenu itself
43
- coords.left + (sideMenuDetection === "editor" ? 50 : 0),
44
- coords.top,
45
- );
40
+ const elements = view.root.elementsFromPoint(coords.left, coords.top);
46
41
 
47
42
  for (const element of elements) {
48
43
  if (!view.dom.contains(element)) {
@@ -55,10 +50,10 @@ function getBlockFromCoords(
55
50
  return getBlockFromCoords(
56
51
  view,
57
52
  {
53
+ // TODO can we do better than this?
58
54
  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
59
55
  top: coords.top,
60
56
  },
61
- sideMenuDetection,
62
57
  false,
63
58
  );
64
59
  }
@@ -74,7 +69,6 @@ function getBlockFromMousePos(
74
69
  y: number;
75
70
  },
76
71
  view: EditorView,
77
- sideMenuDetection: "viewport" | "editor",
78
72
  ): { node: HTMLElement; id: string } | undefined {
79
73
  // Editor itself may have padding or other styling which affects
80
74
  // size/position, so we get the boundingRect of the first child (i.e. the
@@ -88,47 +82,42 @@ function getBlockFromMousePos(
88
82
  view.dom.firstChild as HTMLElement
89
83
  ).getBoundingClientRect();
90
84
 
91
- // this.horizontalPosAnchor = editorBoundingBox.x;
92
-
93
85
  // Gets block at mouse cursor's position.
94
86
  const coords = {
95
- left: mousePos.x,
87
+ // Clamps the x position to the editor's bounding box.
88
+ left: Math.min(
89
+ Math.max(editorBoundingBox.left + 10, mousePos.x),
90
+ editorBoundingBox.right - 10,
91
+ ),
96
92
  top: mousePos.y,
97
93
  };
98
94
 
99
- const mouseLeftOfEditor = coords.left < editorBoundingBox.left;
100
- const mouseRightOfEditor = coords.left > editorBoundingBox.right;
95
+ const referenceBlock = getBlockFromCoords(view, coords);
101
96
 
102
- // Clamps the x position to the editor's bounding box.
103
- if (sideMenuDetection === "viewport") {
104
- if (mouseLeftOfEditor) {
105
- coords.left = editorBoundingBox.left + 10;
106
- }
107
-
108
- if (mouseRightOfEditor) {
109
- coords.left = editorBoundingBox.right - 10;
110
- }
97
+ if (!referenceBlock) {
98
+ // could not find the reference block
99
+ return undefined;
111
100
  }
112
101
 
113
- let block = getBlockFromCoords(view, coords, sideMenuDetection);
114
-
115
- if (!mouseRightOfEditor && block) {
116
- // note: this case is not necessary when we're on the right side of the editor
117
-
118
- /* Now, because blocks can be nested
119
- | BlockA |
120
- x | BlockB y|
121
-
122
- hovering over position x (the "margin of block B") will return block A instead of block B.
123
- to fix this, we get the block from the right side of block A (position y, which will fall in BlockB correctly)
124
- */
125
-
126
- const rect = block.node.getBoundingClientRect();
127
- coords.left = rect.right - 10;
128
- block = getBlockFromCoords(view, coords, "viewport", false);
129
- }
130
-
131
- return block;
102
+ /**
103
+ * Because blocks may be nested, we need to check the right edge of the parent block:
104
+ * ```
105
+ * | BlockA |
106
+ * x | BlockB y|
107
+ * ```
108
+ * Hovering at position x (left edge of BlockB) would return BlockA.
109
+ * Instead, we check at position y (right edge of BlockA) to correctly identify BlockB.
110
+ */
111
+ const referenceBlocksBoundingBox =
112
+ referenceBlock.node.getBoundingClientRect();
113
+ return getBlockFromCoords(
114
+ view,
115
+ {
116
+ left: referenceBlocksBoundingBox.right - 10,
117
+ top: mousePos.y,
118
+ },
119
+ false,
120
+ );
132
121
  }
133
122
 
134
123
  /**
@@ -153,7 +142,6 @@ export class SideMenuView<
153
142
 
154
143
  constructor(
155
144
  private readonly editor: BlockNoteEditor<BSchema, I, S>,
156
- private readonly sideMenuDetection: "viewport" | "editor",
157
145
  private readonly pmView: EditorView,
158
146
  emitUpdate: (state: SideMenuState<BSchema, I, S>) => void,
159
147
  ) {
@@ -215,11 +203,23 @@ export class SideMenuView<
215
203
  return;
216
204
  }
217
205
 
218
- const block = getBlockFromMousePos(
219
- this.mousePos,
220
- this.pmView,
221
- this.sideMenuDetection,
222
- );
206
+ const closestEditor = this.findClosestEditorElement({
207
+ clientX: this.mousePos.x,
208
+ clientY: this.mousePos.y,
209
+ });
210
+
211
+ if (
212
+ closestEditor?.element !== this.pmView.dom ||
213
+ closestEditor.distance > DISTANCE_TO_CONSIDER_EDITOR_BOUNDS
214
+ ) {
215
+ if (this.state?.show) {
216
+ this.state.show = false;
217
+ this.updateState(this.state);
218
+ }
219
+ return;
220
+ }
221
+
222
+ const block = getBlockFromMousePos(this.mousePos, this.pmView);
223
223
 
224
224
  // Closes the menu if the mouse cursor is beyond the editor vertically.
225
225
  if (!block || !this.editor.isEditable) {
@@ -279,95 +279,6 @@ export class SideMenuView<
279
279
  }
280
280
  };
281
281
 
282
- onDrop = (event: DragEvent) => {
283
- // Content from outside a BlockNote editor is being dropped - just let
284
- // ProseMirror's default behaviour handle it.
285
- if (this.pmView.dragging === null) {
286
- return;
287
- }
288
-
289
- this.editor._tiptapEditor.commands.blur();
290
-
291
- // Finds the BlockNote editor element that the drop event occurred in (if
292
- // any).
293
- const parentEditorElement =
294
- event.target instanceof Node
295
- ? (event.target instanceof HTMLElement
296
- ? event.target
297
- : event.target.parentElement
298
- )?.closest(".bn-editor") || null
299
- : null;
300
-
301
- // Drop event occurred within an editor.
302
- if (parentEditorElement) {
303
- // When ProseMirror handles a drop event on the editor while
304
- // `view.dragging` is set, it deletes the selected content. However, if
305
- // a block from a different editor is being dropped, this causes some
306
- // issues that the code below fixes:
307
- if (!this.isDragOrigin && this.pmView.dom === parentEditorElement) {
308
- // Because the editor selection is unrelated to the dragged content, we
309
- // don't want PM to delete its content. Therefore, we collapse the
310
- // selection.
311
- this.pmView.dispatch(
312
- this.pmView.state.tr.setSelection(
313
- TextSelection.create(
314
- this.pmView.state.tr.doc,
315
- this.pmView.state.tr.selection.to,
316
- ),
317
- ),
318
- );
319
- } else if (this.isDragOrigin && this.pmView.dom !== parentEditorElement) {
320
- // Because the editor from which the block originates doesn't get a drop
321
- // event on it, PM doesn't delete its selected content. Therefore, we
322
- // need to do so manually.
323
- //
324
- // Note: Deleting the selected content from the editor from which the
325
- // block originates, may change its height. This can cause the position of
326
- // the editor in which the block is being dropping to shift, before it
327
- // can handle the drop event. That in turn can cause the drop to happen
328
- // somewhere other than the user intended. To get around this, we delay
329
- // deleting the selected content until all editors have had the chance to
330
- // handle the event.
331
- setTimeout(
332
- () => this.pmView.dispatch(this.pmView.state.tr.deleteSelection()),
333
- 0,
334
- );
335
- }
336
- }
337
-
338
- if (
339
- this.sideMenuDetection === "editor" ||
340
- (event as any).synthetic ||
341
- !event.dataTransfer?.types.includes("blocknote/html")
342
- ) {
343
- return;
344
- }
345
-
346
- const pos = this.pmView.posAtCoords({
347
- left: event.clientX,
348
- top: event.clientY,
349
- });
350
-
351
- if (!pos || pos.inside === -1) {
352
- /**
353
- * When `this.sideMenuSelection === "viewport"`, if the event is outside the
354
- * editor contents, we dispatch a fake event, so that we can still drop the
355
- * content when dragging / dropping to the side of the editor
356
- */
357
- const evt = this.createSyntheticEvent(event);
358
- // console.log("dispatch fake drop");
359
- this.pmView.dom.dispatchEvent(evt);
360
- }
361
- };
362
-
363
- onDragEnd = () => {
364
- // When the user starts dragging a block, `view.dragging` is set on all
365
- // BlockNote editors. However, when the drag ends, only the editor that the
366
- // drag originated in automatically clears `view.dragging`. Therefore, we
367
- // have to manually clear it on all editors.
368
- this.pmView.dragging = null;
369
- };
370
-
371
282
  /**
372
283
  * If a block is being dragged, ProseMirror usually gets the context of what's
373
284
  * being dragged from `view.dragging`, which is automatically set when a
@@ -393,7 +304,8 @@ export class SideMenuView<
393
304
  }
394
305
 
395
306
  if (this.pmView.dragging) {
396
- throw new Error("New drag was started while an existing drag is ongoing");
307
+ // already dragging, so no-op
308
+ return;
397
309
  }
398
310
 
399
311
  const element = document.createElement("div");
@@ -411,29 +323,240 @@ export class SideMenuView<
411
323
  };
412
324
 
413
325
  /**
414
- * If the event is outside the editor contents,
415
- * we dispatch a fake event, so that we can still drop the content
416
- * when dragging / dropping to the side of the editor
326
+ * Finds the closest editor visually to the given coordinates
327
+ */
328
+ private findClosestEditorElement = (coords: {
329
+ clientX: number;
330
+ clientY: number;
331
+ }) => {
332
+ // Get all editor elements in the document
333
+ const editors = Array.from(this.pmView.root.querySelectorAll(".bn-editor"));
334
+
335
+ if (editors.length === 0) {
336
+ return null;
337
+ }
338
+
339
+ // Find the editor with the smallest distance to the coordinates
340
+ let closestEditor = editors[0];
341
+ let minDistance = Number.MAX_VALUE;
342
+
343
+ editors.forEach((editor) => {
344
+ const rect = editor
345
+ .querySelector(".bn-block-group")!
346
+ .getBoundingClientRect();
347
+
348
+ const distanceX =
349
+ coords.clientX < rect.left
350
+ ? rect.left - coords.clientX
351
+ : coords.clientX > rect.right
352
+ ? coords.clientX - rect.right
353
+ : 0;
354
+
355
+ const distanceY =
356
+ coords.clientY < rect.top
357
+ ? rect.top - coords.clientY
358
+ : coords.clientY > rect.bottom
359
+ ? coords.clientY - rect.bottom
360
+ : 0;
361
+
362
+ const distance = Math.sqrt(
363
+ Math.pow(distanceX, 2) + Math.pow(distanceY, 2),
364
+ );
365
+
366
+ if (distance < minDistance) {
367
+ minDistance = distance;
368
+ closestEditor = editor;
369
+ }
370
+ });
371
+
372
+ return {
373
+ element: closestEditor,
374
+ distance: minDistance,
375
+ };
376
+ };
377
+
378
+ /**
379
+ * This dragover event handler listens at the document level,
380
+ * and is trying to handle dragover events for all editors.
381
+ *
382
+ * It specifically is trying to handle the following cases:
383
+ * - If the dragover event is within the bounds of any editor, then it does nothing
384
+ * - If the dragover event is outside the bounds of any editor, but close enough (within DISTANCE_TO_CONSIDER_EDITOR_BOUNDS) to the closest editor,
385
+ * then it dispatches a synthetic dragover event to the closest editor (which will trigger the drop-cursor to be shown on that editor)
386
+ * - If the dragover event is outside the bounds of the current editor, then it will dispatch a synthetic dragleave event to the current editor
387
+ * (which will trigger the drop-cursor to be removed from the current editor)
388
+ *
389
+ * The synthetic event is a necessary evil because we do not control prosemirror-dropcursor to be able to show the drop-cursor within the range we want
417
390
  */
418
391
  onDragOver = (event: DragEvent) => {
392
+ if ((event as any).synthetic) {
393
+ return;
394
+ }
395
+
396
+ const dragEventContext = this.getDragEventContext(event);
397
+
398
+ if (!dragEventContext || !dragEventContext.isDropPoint) {
399
+ // This is not a drag event that we are interested in
400
+ // so, we close the drop-cursor
401
+ this.closeDropCursor();
402
+ return;
403
+ }
404
+
405
+ if (
406
+ dragEventContext.isDropPoint &&
407
+ !dragEventContext.isDropWithinEditorBounds
408
+ ) {
409
+ // we are the drop point, but the drag over event is not within the bounds of this editor instance
410
+ // so, we need to dispatch an event that is in the bounds of this editor instance
411
+ this.dispatchSyntheticEvent(event);
412
+ }
413
+ };
414
+
415
+ /**
416
+ * Closes the drop-cursor for the current editor
417
+ */
418
+ private closeDropCursor = () => {
419
+ const evt = new Event("dragleave", { bubbles: false });
420
+ // It needs to be synthetic, so we don't accidentally think it is a real dragend event
421
+ (evt as any).synthetic = true;
422
+ // We dispatch the event to the current editor, so that the drop-cursor is removed for it
423
+ this.pmView.dom.dispatchEvent(evt);
424
+ };
425
+
426
+ /**
427
+ * It is surprisingly difficult to determine the information we need to know about a drag event
428
+ *
429
+ * This function is trying to determine the following:
430
+ * - Whether the current editor instance is the drop point
431
+ * - Whether the current editor instance is the drag origin
432
+ * - Whether the drop event is within the bounds of the current editor instance
433
+ */
434
+ getDragEventContext = (event: DragEvent) => {
435
+ // We need to check if there is text content that is being dragged (select some text & just drag it)
436
+ const textContentIsBeingDragged =
437
+ !event.dataTransfer?.types.includes("blocknote/html") &&
438
+ Boolean(this.pmView.dragging);
439
+ // This is the side menu drag from this plugin
440
+ const sideMenuIsBeingDragged = Boolean(this.isDragOrigin);
441
+ // Tells us that the current editor instance has a drag ongoing (either text or side menu)
442
+ const isDragOrigin = textContentIsBeingDragged || sideMenuIsBeingDragged;
443
+
444
+ // Tells us which editor instance is the closest to the drag event (whether or not it is actually reasonably close)
445
+ const closestEditor = this.findClosestEditorElement(event);
446
+
447
+ // We arbitrarily decide how far is "too far" from the closest editor to be considered a drop point
419
448
  if (
420
- this.sideMenuDetection === "editor" ||
421
- (event as any).synthetic ||
422
- !event.dataTransfer?.types.includes("blocknote/html")
449
+ !closestEditor ||
450
+ closestEditor.distance > DISTANCE_TO_CONSIDER_EDITOR_BOUNDS
423
451
  ) {
452
+ // we are too far from the closest editor, or no editor was found
453
+ return undefined;
454
+ }
455
+
456
+ // We check if the closest editor is the same as the current editor instance (which is the drop point)
457
+ const isDropPoint = closestEditor.element === this.pmView.dom;
458
+ // We check if the current editor instance is the same as the editor instance that the drag event is happening within
459
+ const isDropWithinEditorBounds =
460
+ isDropPoint && closestEditor.distance === 0;
461
+
462
+ // We never want to handle drop events that are not related to us
463
+ if (!isDropPoint && !isDragOrigin) {
464
+ // we are not the drop point or drag origin, so not relevant to us
465
+ return undefined;
466
+ }
467
+
468
+ return {
469
+ isDropPoint,
470
+ isDropWithinEditorBounds,
471
+ isDragOrigin,
472
+ };
473
+ };
474
+
475
+ /**
476
+ * The drop event handler listens at the document level,
477
+ * and handles drop events for all editors.
478
+ *
479
+ * It specifically handles the following cases:
480
+ * - If we are both the drag origin and drop point:
481
+ * - Let normal drop handling take over
482
+ * - If we are the drop point but not the drag origin:
483
+ * - Collapse selection to prevent PM from deleting unrelated content
484
+ * - If drop event is outside our editor bounds, dispatch synthetic drop event to our editor
485
+ * - If we are the drag origin but not the drop point:
486
+ * - Delete the dragged content from our editor after a delay
487
+ */
488
+ onDrop = (event: DragEvent) => {
489
+ if ((event as any).synthetic) {
424
490
  return;
425
491
  }
426
492
 
427
- const pos = this.pmView.posAtCoords({
428
- left: event.clientX,
429
- top: event.clientY,
430
- });
493
+ const context = this.getDragEventContext(event);
494
+ if (!context) {
495
+ this.closeDropCursor();
496
+ // This is not a drag event that we are interested in
497
+ return;
498
+ }
499
+ const { isDropPoint, isDropWithinEditorBounds, isDragOrigin } = context;
500
+
501
+ if (!isDropWithinEditorBounds) {
502
+ // Any time that the drop event is outside of the editor bounds (but still close to an editor instance)
503
+ // We dispatch a synthetic event that is in the bounds of the editor instance, to have the correct drop point
504
+ this.dispatchSyntheticEvent(event);
505
+ }
506
+
507
+ if (isDropPoint && isDragOrigin) {
508
+ // The current instance is both the drop point and the drag origin
509
+ // no-op, normal drop handling will take over
510
+ return;
511
+ }
512
+
513
+ if (isDropPoint) {
514
+ // The current instance is the drop point, but not the drag origin
515
+
516
+ // Because the editor selection is unrelated to the dragged content, we
517
+ // don't want PM to delete its content. Therefore, we collapse the
518
+ // selection.
519
+ this.pmView.dispatch(
520
+ this.pmView.state.tr.setSelection(
521
+ TextSelection.create(
522
+ this.pmView.state.tr.doc,
523
+ this.pmView.state.tr.selection.anchor,
524
+ ),
525
+ ),
526
+ );
527
+ return;
528
+ } else if (isDragOrigin) {
529
+ // The current instance is the drag origin, but not the drop point
530
+ // our content got dropped somewhere else
531
+
532
+ // Because the editor from which the block originates doesn't get a drop
533
+ // event on it, PM doesn't delete its selected content. Therefore, we
534
+ // need to do so manually.
535
+ //
536
+ // Note: Deleting the selected content from the editor from which the
537
+ // block originates, may change its height. This can cause the position of
538
+ // the editor in which the block is being dropping to shift, before it
539
+ // can handle the drop event. That in turn can cause the drop to happen
540
+ // somewhere other than the user intended. To get around this, we delay
541
+ // deleting the selected content until all editors have had the chance to
542
+ // handle the event.
543
+ setTimeout(
544
+ () => this.pmView.dispatch(this.pmView.state.tr.deleteSelection()),
545
+ 0,
546
+ );
547
+ return;
548
+ }
549
+ };
431
550
 
432
- if (!pos || (pos.inside === -1 && this.pmView.dom.firstChild)) {
433
- const evt = this.createSyntheticEvent(event);
434
- // console.log("dispatch fake dragover");
435
- this.pmView.dom.dispatchEvent(evt);
551
+ onDragEnd = (event: DragEvent) => {
552
+ if ((event as any).synthetic) {
553
+ return;
436
554
  }
555
+ // When the user starts dragging a block, `view.dragging` is set on all
556
+ // BlockNote editors. However, when the drag ends, only the editor that the
557
+ // drag originated in automatically clears `view.dragging`. Therefore, we
558
+ // have to manually clear it on all editors.
559
+ this.pmView.dragging = null;
437
560
  };
438
561
 
439
562
  onKeyDown = (_event: KeyboardEvent) => {
@@ -488,60 +611,27 @@ export class SideMenuView<
488
611
  this.updateStateFromMousePos();
489
612
  };
490
613
 
491
- private createSyntheticEvent(event: DragEvent) {
492
- const evt = new Event(event.type, event) as any;
493
- const editorBoundingBox = (
614
+ private dispatchSyntheticEvent(event: DragEvent) {
615
+ const evt = new Event(event.type as "dragover", event) as any;
616
+ const dropPointBoundingBox = (
494
617
  this.pmView.dom.firstChild as HTMLElement
495
618
  ).getBoundingClientRect();
496
619
  evt.clientX = event.clientX;
497
620
  evt.clientY = event.clientY;
498
- if (
499
- event.clientX < editorBoundingBox.left &&
500
- event.clientX >
501
- editorBoundingBox.left -
502
- editorBoundingBox.width *
503
- PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP
504
- ) {
505
- // when we're slightly left of the editor, we can drop to the side of the block
506
- evt.clientX =
507
- editorBoundingBox.left +
508
- (editorBoundingBox.width *
509
- PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP) /
510
- 2;
511
- } else if (
512
- event.clientX > editorBoundingBox.right &&
513
- event.clientX <
514
- editorBoundingBox.right +
515
- editorBoundingBox.width *
516
- PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP
517
- ) {
518
- // when we're slightly right of the editor, we can drop to the side of the block
519
- evt.clientX =
520
- editorBoundingBox.right -
521
- (editorBoundingBox.width *
522
- PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP) /
523
- 2;
524
- } else if (
525
- event.clientX < editorBoundingBox.left ||
526
- event.clientX > editorBoundingBox.right
527
- ) {
528
- // when mouse is outside of the editor on x axis, drop it somewhere safe (but not to the side of a block)
529
- evt.clientX =
530
- editorBoundingBox.left +
531
- PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP *
532
- editorBoundingBox.width *
533
- 2; // put it somewhere in first block, but safe outside of the PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP margin
534
- }
535
621
 
622
+ evt.clientX = Math.min(
623
+ Math.max(event.clientX, dropPointBoundingBox.left),
624
+ dropPointBoundingBox.left + dropPointBoundingBox.width,
625
+ );
536
626
  evt.clientY = Math.min(
537
- Math.max(event.clientY, editorBoundingBox.top),
538
- editorBoundingBox.top + editorBoundingBox.height,
627
+ Math.max(event.clientY, dropPointBoundingBox.top),
628
+ dropPointBoundingBox.top + dropPointBoundingBox.height,
539
629
  );
540
630
 
541
631
  evt.dataTransfer = event.dataTransfer;
542
632
  evt.preventDefault = () => event.preventDefault();
543
633
  evt.synthetic = true; // prevent recursion
544
- return evt;
634
+ this.pmView.dom.dispatchEvent(evt);
545
635
  }
546
636
 
547
637
  onScroll = () => {
@@ -549,6 +639,7 @@ export class SideMenuView<
549
639
  this.state.referencePos = this.hoveredBlock!.getBoundingClientRect();
550
640
  this.emitUpdate(this.state);
551
641
  }
642
+ this.updateStateFromMousePos();
552
643
  };
553
644
 
554
645
  // Needed in cases where the editor state updates without the mouse cursor
@@ -615,23 +706,15 @@ export class SideMenuProsemirrorPlugin<
615
706
 
616
707
  public view: SideMenuView<BSchema, I, S> | undefined;
617
708
 
618
- constructor(
619
- private readonly editor: BlockNoteEditor<BSchema, I, S>,
620
- sideMenuDetection: "viewport" | "editor",
621
- ) {
709
+ constructor(private readonly editor: BlockNoteEditor<BSchema, I, S>) {
622
710
  super();
623
711
  this.addProsemirrorPlugin(
624
712
  new Plugin({
625
713
  key: sideMenuPluginKey,
626
714
  view: (editorView) => {
627
- this.view = new SideMenuView(
628
- editor,
629
- sideMenuDetection,
630
- editorView,
631
- (state) => {
632
- this.emit("update", state);
633
- },
634
- );
715
+ this.view = new SideMenuView(editor, editorView, (state) => {
716
+ this.emit("update", state);
717
+ });
635
718
  return this.view;
636
719
  },
637
720
  }),
@@ -5,6 +5,7 @@ import { StyleSchema, Styles } from "../styles/types.js";
5
5
  export type CustomInlineContentConfig = {
6
6
  type: string;
7
7
  content: "styled" | "none"; // | "plain"
8
+ draggable?: boolean;
8
9
  readonly propSchema: PropSchema;
9
10
  // content: "inline" | "none" | "table";
10
11
  };
@@ -22,6 +23,13 @@ export type InlineContentImplementation<T extends InlineContentConfig> =
22
23
  node: Node;
23
24
  };
24
25
 
26
+ export type InlineContentSchemaWithInlineContent<
27
+ IType extends string,
28
+ C extends InlineContentConfig,
29
+ > = {
30
+ [k in IType]: C;
31
+ };
32
+
25
33
  // Container for both the config and implementation of InlineContent,
26
34
  // and the type of `implementation` is based on that of the config
27
35
  export type InlineContentSpec<T extends InlineContentConfig> = {