@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.
- package/dist/blocknote.cjs +9 -9
- package/dist/blocknote.cjs.map +1 -1
- package/dist/blocknote.js +1838 -1715
- package/dist/blocknote.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +2 -2
- package/src/api/__snapshots__/blocks-indented-changed.json +129 -0
- package/src/api/__snapshots__/blocks-moved-deeper-into-nesting.json +164 -0
- package/src/api/__snapshots__/blocks-moved-multiple-in-same-transaction.json +188 -0
- package/src/api/__snapshots__/blocks-moved-to-different-parent.json +78 -0
- package/src/api/__snapshots__/blocks-moved-to-root-level.json +78 -0
- package/src/api/__snapshots__/blocks-outdented-changed.json +129 -0
- package/src/api/blockManipulation/commands/nestBlock/nestBlock.ts +58 -59
- package/src/api/nodeUtil.test.ts +228 -1
- package/src/api/nodeUtil.ts +135 -118
- package/src/api/parsers/markdown/detectMarkdown.test.ts +211 -0
- package/src/api/parsers/markdown/detectMarkdown.ts +3 -2
- package/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +2 -1
- package/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +2 -1
- package/src/blocks/ToggleWrapper/createToggleWrapper.ts +2 -0
- package/src/blocks/defaultBlockTypeGuards.ts +30 -0
- package/src/editor/BlockNoteEditor.ts +27 -10
- package/src/editor/BlockNoteExtensions.ts +3 -5
- package/src/exporter/Exporter.ts +2 -0
- package/src/exporter/mapping.ts +1 -0
- package/src/extensions/BlockChange/BlockChangePlugin.ts +66 -0
- package/src/extensions/SideMenu/SideMenuPlugin.ts +290 -207
- package/src/schema/inlineContent/types.ts +8 -0
- package/types/src/api/nodeUtil.d.ts +15 -21
- package/types/src/api/parsers/markdown/detectMarkdown.test.d.ts +1 -0
- package/types/src/blocks/defaultBlockTypeGuards.d.ts +7 -1
- package/types/src/editor/BlockNoteEditor.d.ts +11 -8
- package/types/src/editor/BlockNoteExtensions.d.ts +0 -1
- package/types/src/exporter/Exporter.d.ts +1 -1
- package/types/src/exporter/mapping.d.ts +1 -1
- package/types/src/extensions/BlockChange/BlockChangePlugin.d.ts +15 -0
- package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +50 -9
- 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
|
|
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
|
-
|
|
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
|
|
100
|
-
const mouseRightOfEditor = coords.left > editorBoundingBox.right;
|
|
95
|
+
const referenceBlock = getBlockFromCoords(view, coords);
|
|
101
96
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
219
|
-
this.mousePos,
|
|
220
|
-
this.
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
421
|
-
|
|
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
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
492
|
-
const evt = new Event(event.type, event) as any;
|
|
493
|
-
const
|
|
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,
|
|
538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
629
|
-
|
|
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> = {
|