@blocknote/core 0.5.0 → 0.6.1

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.js +1126 -1031
  2. package/dist/blocknote.js.map +1 -1
  3. package/dist/blocknote.umd.cjs +2 -2
  4. package/dist/blocknote.umd.cjs.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +2 -2
  7. package/src/BlockNoteEditor.ts +244 -3
  8. package/src/BlockNoteExtensions.ts +3 -2
  9. package/src/api/blockManipulation/__snapshots__/blockManipulation.test.ts.snap +35 -35
  10. package/src/api/formatConversions/__snapshots__/formatConversions.test.ts.snap +10 -10
  11. package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +10 -10
  12. package/src/api/nodeConversions/nodeConversions.ts +11 -10
  13. package/src/editor.module.css +12 -3
  14. package/src/extensions/Blocks/api/inlineContentTypes.ts +3 -2
  15. package/src/extensions/Blocks/api/selectionTypes.ts +5 -0
  16. package/src/extensions/Blocks/nodes/Block.module.css +20 -2
  17. package/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts +5 -6
  18. package/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +5 -2
  19. package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +76 -113
  20. package/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts +6 -3
  21. package/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +2 -34
  22. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +39 -113
  23. package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +56 -39
  24. package/src/extensions/UniqueID/UniqueID.ts +1 -1
  25. package/src/index.ts +1 -0
  26. package/src/shared/plugins/suggestion/SuggestionPlugin.ts +14 -2
  27. package/types/src/BlockNoteEditor.d.ts +79 -0
  28. package/types/src/extensions/Blocks/api/inlineContentTypes.d.ts +3 -2
  29. package/types/src/extensions/Blocks/api/selectionTypes.d.ts +4 -0
  30. package/types/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.d.ts +4 -5
  31. package/types/src/extensions/DraggableBlocks/DraggableBlocksExtension.d.ts +3 -1
  32. package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +14 -16
  33. package/types/src/extensions/FormattingToolbar/FormattingToolbarExtension.d.ts +2 -0
  34. package/types/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.d.ts +2 -25
  35. package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +9 -4
  36. package/types/src/index.d.ts +1 -0
  37. package/types/src/api/Editor.d.ts +0 -93
  38. package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +0 -24
  39. package/types/src/extensions/SlashMenu/defaultSlashCommands.d.ts +0 -5
@@ -14,26 +14,13 @@ import {
14
14
  } from "./BlockSideMenuFactoryTypes";
15
15
  import { DraggableBlocksOptions } from "./DraggableBlocksExtension";
16
16
  import { MultipleNodeSelection } from "./MultipleNodeSelection";
17
+ import { BlockNoteEditor } from "../../BlockNoteEditor";
17
18
 
18
19
  const serializeForClipboard = (pv as any).__serializeForClipboard;
19
20
  // code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799
20
21
 
21
22
  let dragImageElement: Element | undefined;
22
23
 
23
- export function createRect(rect: DOMRect) {
24
- let newRect = {
25
- left: rect.left + document.body.scrollLeft,
26
- top: rect.top + document.body.scrollTop,
27
- width: rect.width,
28
- height: rect.height,
29
- bottom: 0,
30
- right: 0,
31
- };
32
- newRect.bottom = newRect.top + newRect.height;
33
- newRect.right = newRect.left + newRect.width;
34
- return newRect;
35
- }
36
-
37
24
  function getDraggableBlockFromCoords(
38
25
  coords: { left: number; top: number },
39
26
  view: EditorView
@@ -208,9 +195,9 @@ function dragStart(e: DragEvent, view: EditorView) {
208
195
  const { from, to } = blockPositionsFromSelection(selection, doc);
209
196
 
210
197
  const draggedBlockInSelection = from <= pos && pos < to;
211
- const multipleBlocksSelected = !selection.$anchor
212
- .node()
213
- .eq(selection.$head.node());
198
+ const multipleBlocksSelected =
199
+ selection.$anchor.node() !== selection.$head.node() ||
200
+ selection instanceof MultipleNodeSelection;
214
201
 
215
202
  if (draggedBlockInSelection && multipleBlocksSelected) {
216
203
  view.dispatch(
@@ -237,13 +224,15 @@ function dragStart(e: DragEvent, view: EditorView) {
237
224
  }
238
225
 
239
226
  export type BlockMenuViewProps = {
240
- editor: Editor;
227
+ tiptapEditor: Editor;
228
+ editor: BlockNoteEditor;
241
229
  blockMenuFactory: BlockSideMenuFactory;
242
230
  horizontalPosAnchoredAtRoot: boolean;
243
231
  };
244
232
 
245
233
  export class BlockMenuView {
246
- editor: Editor;
234
+ editor: BlockNoteEditor;
235
+ private ttEditor: Editor;
247
236
 
248
237
  // When true, the drag handle with be anchored at the same level as root elements
249
238
  // When false, the drag handle with be just to the left of the element
@@ -253,30 +242,38 @@ export class BlockMenuView {
253
242
 
254
243
  blockMenu: BlockSideMenu;
255
244
 
256
- hoveredBlockContent: HTMLElement | undefined;
245
+ hoveredBlock: HTMLElement | undefined;
257
246
 
247
+ // Used to check if currently dragged content comes from this editor instance.
248
+ isDragging = false;
258
249
  menuOpen = false;
259
250
  menuFrozen = false;
260
251
 
261
252
  constructor({
253
+ tiptapEditor,
262
254
  editor,
263
255
  blockMenuFactory,
264
256
  horizontalPosAnchoredAtRoot,
265
257
  }: BlockMenuViewProps) {
266
258
  this.editor = editor;
259
+ this.ttEditor = tiptapEditor;
267
260
  this.horizontalPosAnchoredAtRoot = horizontalPosAnchoredAtRoot;
268
261
  this.horizontalPosAnchor = (
269
- editor.view.dom.firstChild! as HTMLElement
262
+ this.ttEditor.view.dom.firstChild! as HTMLElement
270
263
  ).getBoundingClientRect().x;
271
264
 
272
265
  this.blockMenu = blockMenuFactory(this.getStaticParams());
273
266
 
274
267
  document.body.addEventListener("drop", this.onDrop, true);
275
268
  document.body.addEventListener("dragover", this.onDragOver);
269
+ this.ttEditor.view.dom.addEventListener("dragstart", this.onDragStart);
276
270
 
277
271
  // Shows or updates menu position whenever the cursor moves, if the menu isn't frozen.
278
272
  document.body.addEventListener("mousemove", this.onMouseMove, true);
279
273
 
274
+ // Makes menu scroll with the page.
275
+ document.addEventListener("scroll", this.onScroll);
276
+
280
277
  // Hides and unfreezes the menu whenever the user selects the editor with the mouse or presses a key.
281
278
  // TODO: Better integration with suggestions menu and only editor scope?
282
279
  document.body.addEventListener("mousedown", this.onMouseDown, true);
@@ -284,23 +281,32 @@ export class BlockMenuView {
284
281
  }
285
282
 
286
283
  /**
287
- * If the event is outside of the editor contents,
284
+ * Sets isDragging when dragging text.
285
+ */
286
+ onDragStart = () => {
287
+ this.isDragging = true;
288
+ };
289
+
290
+ /**
291
+ * If the event is outside the editor contents,
288
292
  * we dispatch a fake event, so that we can still drop the content
289
293
  * when dragging / dropping to the side of the editor
290
294
  */
291
295
  onDrop = (event: DragEvent) => {
292
- if ((event as any).synthetic) {
296
+ if ((event as any).synthetic || !this.isDragging) {
293
297
  return;
294
298
  }
295
- let pos = this.editor.view.posAtCoords({
299
+ let pos = this.ttEditor.view.posAtCoords({
296
300
  left: event.clientX,
297
301
  top: event.clientY,
298
302
  });
299
303
 
304
+ this.isDragging = false;
305
+
300
306
  if (!pos || pos.inside === -1) {
301
307
  const evt = new Event("drop", event) as any;
302
308
  const editorBoundingBox = (
303
- this.editor.view.dom.firstChild! as HTMLElement
309
+ this.ttEditor.view.dom.firstChild! as HTMLElement
304
310
  ).getBoundingClientRect();
305
311
  evt.clientX = editorBoundingBox.left + editorBoundingBox.width / 2;
306
312
  evt.clientY = event.clientY;
@@ -308,7 +314,7 @@ export class BlockMenuView {
308
314
  evt.preventDefault = () => event.preventDefault();
309
315
  evt.synthetic = true; // prevent recursion
310
316
  // console.log("dispatch fake drop");
311
- this.editor.view.dom.dispatchEvent(evt);
317
+ this.ttEditor.view.dom.dispatchEvent(evt);
312
318
  }
313
319
  };
314
320
 
@@ -318,10 +324,10 @@ export class BlockMenuView {
318
324
  * when dragging / dropping to the side of the editor
319
325
  */
320
326
  onDragOver = (event: DragEvent) => {
321
- if ((event as any).synthetic) {
327
+ if ((event as any).synthetic || !this.isDragging) {
322
328
  return;
323
329
  }
324
- let pos = this.editor.view.posAtCoords({
330
+ let pos = this.ttEditor.view.posAtCoords({
325
331
  left: event.clientX,
326
332
  top: event.clientY,
327
333
  });
@@ -329,7 +335,7 @@ export class BlockMenuView {
329
335
  if (!pos || pos.inside === -1) {
330
336
  const evt = new Event("dragover", event) as any;
331
337
  const editorBoundingBox = (
332
- this.editor.view.dom.firstChild! as HTMLElement
338
+ this.ttEditor.view.dom.firstChild! as HTMLElement
333
339
  ).getBoundingClientRect();
334
340
  evt.clientX = editorBoundingBox.left + editorBoundingBox.width / 2;
335
341
  evt.clientY = event.clientY;
@@ -337,7 +343,7 @@ export class BlockMenuView {
337
343
  evt.preventDefault = () => event.preventDefault();
338
344
  evt.synthetic = true; // prevent recursion
339
345
  // console.log("dispatch fake dragover");
340
- this.editor.view.dom.dispatchEvent(evt);
346
+ this.ttEditor.view.dom.dispatchEvent(evt);
341
347
  }
342
348
  };
343
349
 
@@ -371,7 +377,7 @@ export class BlockMenuView {
371
377
  // Editor itself may have padding or other styling which affects size/position, so we get the boundingRect of
372
378
  // the first child (i.e. the blockGroup that wraps all blocks in the editor) for a more accurate bounding box.
373
379
  const editorBoundingBox = (
374
- this.editor.view.dom.firstChild! as HTMLElement
380
+ this.ttEditor.view.dom.firstChild! as HTMLElement
375
381
  ).getBoundingClientRect();
376
382
 
377
383
  this.horizontalPosAnchor = editorBoundingBox.x;
@@ -381,10 +387,10 @@ export class BlockMenuView {
381
387
  left: editorBoundingBox.left + editorBoundingBox.width / 2, // take middle of editor
382
388
  top: event.clientY,
383
389
  };
384
- const block = getDraggableBlockFromCoords(coords, this.editor.view);
390
+ const block = getDraggableBlockFromCoords(coords, this.ttEditor.view);
385
391
 
386
392
  // Closes the menu if the mouse cursor is beyond the editor vertically.
387
- if (!block) {
393
+ if (!block || !this.editor.isEditable) {
388
394
  if (this.menuOpen) {
389
395
  this.menuOpen = false;
390
396
  this.blockMenu.hide();
@@ -396,25 +402,34 @@ export class BlockMenuView {
396
402
  // Doesn't update if the menu is already open and the mouse cursor is still hovering the same block.
397
403
  if (
398
404
  this.menuOpen &&
399
- this.hoveredBlockContent?.hasAttribute("data-id") &&
400
- this.hoveredBlockContent?.getAttribute("data-id") === block.id
405
+ this.hoveredBlock?.hasAttribute("data-id") &&
406
+ this.hoveredBlock?.getAttribute("data-id") === block.id
401
407
  ) {
402
408
  return;
403
409
  }
404
410
 
411
+ this.hoveredBlock = block.node;
412
+
405
413
  // Gets the block's content node, which lets to ignore child blocks when determining the block menu's position.
406
414
  const blockContent = block.node.firstChild as HTMLElement;
407
- this.hoveredBlockContent = blockContent;
408
415
 
409
416
  if (!blockContent) {
410
417
  return;
411
418
  }
412
419
 
413
420
  // Shows or updates elements.
414
- if (!this.menuOpen) {
415
- this.menuOpen = true;
416
- this.blockMenu.render(this.getDynamicParams(), true);
417
- } else {
421
+ if (this.editor.isEditable) {
422
+ if (!this.menuOpen) {
423
+ this.menuOpen = true;
424
+ this.blockMenu.render(this.getDynamicParams(), true);
425
+ } else {
426
+ this.blockMenu.render(this.getDynamicParams(), false);
427
+ }
428
+ }
429
+ };
430
+
431
+ onScroll = () => {
432
+ if (this.menuOpen) {
418
433
  this.blockMenu.render(this.getDynamicParams(), false);
419
434
  }
420
435
  };
@@ -426,8 +441,10 @@ export class BlockMenuView {
426
441
  }
427
442
  document.body.removeEventListener("mousemove", this.onMouseMove);
428
443
  document.body.removeEventListener("dragover", this.onDragOver);
444
+ this.ttEditor.view.dom.removeEventListener("dragstart", this.onDragStart);
429
445
  document.body.removeEventListener("drop", this.onDrop);
430
446
  document.body.removeEventListener("mousedown", this.onMouseDown);
447
+ document.removeEventListener("scroll", this.onScroll);
431
448
  document.body.removeEventListener("keydown", this.onKeyDown);
432
449
  }
433
450
 
@@ -436,10 +453,10 @@ export class BlockMenuView {
436
453
  this.menuFrozen = true;
437
454
  this.blockMenu.hide();
438
455
 
439
- const blockContentBoundingBox =
440
- this.hoveredBlockContent!.getBoundingClientRect();
456
+ const blockContent = this.hoveredBlock!.firstChild! as HTMLElement;
457
+ const blockContentBoundingBox = blockContent.getBoundingClientRect();
441
458
 
442
- const pos = this.editor.view.posAtCoords({
459
+ const pos = this.ttEditor.view.posAtCoords({
443
460
  left: blockContentBoundingBox.left + blockContentBoundingBox.width / 2,
444
461
  top: blockContentBoundingBox.top + blockContentBoundingBox.height / 2,
445
462
  });
@@ -447,7 +464,7 @@ export class BlockMenuView {
447
464
  return;
448
465
  }
449
466
 
450
- const blockInfo = getBlockInfoFromPos(this.editor.state.doc, pos.pos);
467
+ const blockInfo = getBlockInfoFromPos(this.ttEditor.state.doc, pos.pos);
451
468
  if (blockInfo === undefined) {
452
469
  return;
453
470
  }
@@ -459,20 +476,20 @@ export class BlockMenuView {
459
476
  const newBlockInsertionPos = endPos + 1;
460
477
  const newBlockContentPos = newBlockInsertionPos + 2;
461
478
 
462
- this.editor
479
+ this.ttEditor
463
480
  .chain()
464
481
  .BNCreateBlock(newBlockInsertionPos)
465
482
  .BNUpdateBlock(newBlockContentPos, { type: "paragraph", props: {} })
466
483
  .setTextSelection(newBlockContentPos)
467
484
  .run();
468
485
  } else {
469
- this.editor.commands.setTextSelection(endPos);
486
+ this.ttEditor.commands.setTextSelection(endPos);
470
487
  }
471
488
 
472
489
  // Focuses and activates the suggestion menu.
473
- this.editor.view.focus();
474
- this.editor.view.dispatch(
475
- this.editor.view.state.tr.scrollIntoView().setMeta(SlashMenuPluginKey, {
490
+ this.ttEditor.view.focus();
491
+ this.ttEditor.view.dispatch(
492
+ this.ttEditor.view.state.tr.scrollIntoView().setMeta(SlashMenuPluginKey, {
476
493
  // TODO import suggestion plugin key
477
494
  activate: true,
478
495
  type: "drag",
@@ -480,65 +497,15 @@ export class BlockMenuView {
480
497
  );
481
498
  }
482
499
 
483
- deleteBlock() {
484
- this.menuOpen = false;
485
- this.blockMenu.hide();
486
-
487
- const blockContentBoundingBox =
488
- this.hoveredBlockContent!.getBoundingClientRect();
489
-
490
- const pos = this.editor.view.posAtCoords({
491
- left: blockContentBoundingBox.left + blockContentBoundingBox.width / 2,
492
- top: blockContentBoundingBox.top + blockContentBoundingBox.height / 2,
493
- });
494
- if (!pos) {
495
- return;
496
- }
497
-
498
- this.editor.commands.BNDeleteBlock(pos.pos);
499
- }
500
-
501
- setBlockBackgroundColor(color: string) {
502
- this.menuOpen = false;
503
- this.blockMenu.hide();
504
-
505
- const blockContentBoundingBox =
506
- this.hoveredBlockContent!.getBoundingClientRect();
507
-
508
- const pos = this.editor.view.posAtCoords({
509
- left: blockContentBoundingBox.left + blockContentBoundingBox.width / 2,
510
- top: blockContentBoundingBox.top + blockContentBoundingBox.height / 2,
511
- });
512
- if (!pos) {
513
- return;
514
- }
515
-
516
- this.editor.commands.setBlockBackgroundColor(pos.pos, color);
517
- }
518
-
519
- setBlockTextColor(color: string) {
520
- this.menuOpen = false;
521
- this.blockMenu.hide();
522
-
523
- const blockContentBoundingBox =
524
- this.hoveredBlockContent!.getBoundingClientRect();
525
-
526
- const pos = this.editor.view.posAtCoords({
527
- left: blockContentBoundingBox.left + blockContentBoundingBox.width / 2,
528
- top: blockContentBoundingBox.top + blockContentBoundingBox.height / 2,
529
- });
530
- if (!pos) {
531
- return;
532
- }
533
-
534
- this.editor.commands.setBlockTextColor(pos.pos, color);
535
- }
536
-
537
500
  getStaticParams(): BlockSideMenuStaticParams {
538
501
  return {
502
+ editor: this.editor,
539
503
  addBlock: () => this.addBlock(),
540
- deleteBlock: () => this.deleteBlock(),
541
- blockDragStart: (event: DragEvent) => dragStart(event, this.editor.view),
504
+ blockDragStart: (event: DragEvent) => {
505
+ // Sets isDragging when dragging blocks.
506
+ this.isDragging = true;
507
+ dragStart(event, this.ttEditor.view);
508
+ },
542
509
  blockDragEnd: () => unsetDragImage(),
543
510
  freezeMenu: () => {
544
511
  this.menuFrozen = true;
@@ -546,20 +513,15 @@ export class BlockMenuView {
546
513
  unfreezeMenu: () => {
547
514
  this.menuFrozen = false;
548
515
  },
549
- setBlockBackgroundColor: (color: string) =>
550
- this.setBlockBackgroundColor(color),
551
- setBlockTextColor: (color: string) => this.setBlockTextColor(color),
552
516
  };
553
517
  }
554
518
 
555
519
  getDynamicParams(): BlockSideMenuDynamicParams {
556
- const blockContentBoundingBox =
557
- this.hoveredBlockContent!.getBoundingClientRect();
520
+ const blockContent = this.hoveredBlock!.firstChild! as HTMLElement;
521
+ const blockContentBoundingBox = blockContent.getBoundingClientRect();
558
522
 
559
523
  return {
560
- blockBackgroundColor:
561
- this.editor.getAttributes("blockContainer").backgroundColor,
562
- blockTextColor: this.editor.getAttributes("blockContainer").textColor,
524
+ block: this.editor.getBlock(this.hoveredBlock!.getAttribute("data-id")!)!,
563
525
  referenceRect: new DOMRect(
564
526
  this.horizontalPosAnchoredAtRoot
565
527
  ? this.horizontalPosAnchor
@@ -579,6 +541,7 @@ export const createDraggableBlocksPlugin = (
579
541
  key: new PluginKey("DraggableBlocksPlugin"),
580
542
  view: () =>
581
543
  new BlockMenuView({
544
+ tiptapEditor: options.tiptapEditor,
582
545
  editor: options.editor,
583
546
  blockMenuFactory: options.blockSideMenuFactory,
584
547
  horizontalPosAnchoredAtRoot: true,
@@ -1,5 +1,6 @@
1
1
  import { Extension } from "@tiptap/core";
2
2
  import { PluginKey } from "prosemirror-state";
3
+ import { BlockNoteEditor } from "../..";
3
4
  import { FormattingToolbarFactory } from "./FormattingToolbarFactoryTypes";
4
5
  import { createFormattingToolbarPlugin } from "./FormattingToolbarPlugin";
5
6
 
@@ -8,19 +9,21 @@ import { createFormattingToolbarPlugin } from "./FormattingToolbarPlugin";
8
9
  */
9
10
  export const FormattingToolbarExtension = Extension.create<{
10
11
  formattingToolbarFactory: FormattingToolbarFactory;
12
+ editor: BlockNoteEditor;
11
13
  }>({
12
14
  name: "FormattingToolbarExtension",
13
15
 
14
16
  addProseMirrorPlugins() {
15
- if (!this.options.formattingToolbarFactory) {
17
+ if (!this.options.formattingToolbarFactory || !this.options.editor) {
16
18
  throw new Error(
17
- "UI Element factory not defined for FormattingToolbarExtension"
19
+ "required args not defined for FormattingToolbarExtension"
18
20
  );
19
21
  }
20
22
 
21
23
  return [
22
24
  createFormattingToolbarPlugin({
23
- editor: this.editor,
25
+ tiptapEditor: this.editor,
26
+ editor: this.options.editor,
24
27
  formattingToolbarFactory: this.options.formattingToolbarFactory,
25
28
  pluginKey: new PluginKey("FormattingToolbarPlugin"),
26
29
  }),
@@ -1,43 +1,11 @@
1
1
  import { EditorElement, ElementFactory } from "../../shared/EditorElement";
2
- import { Block, PartialBlock } from "../Blocks/api/blockTypes";
2
+ import { BlockNoteEditor } from "../../BlockNoteEditor";
3
3
 
4
4
  export type FormattingToolbarStaticParams = {
5
- toggleBold: () => void;
6
- toggleItalic: () => void;
7
- toggleUnderline: () => void;
8
- toggleStrike: () => void;
9
- setHyperlink: (url: string, text?: string) => void;
10
-
11
- setTextColor: (color: string) => void;
12
- setBackgroundColor: (color: string) => void;
13
- setTextAlignment: (
14
- textAlignment: "left" | "center" | "right" | "justify"
15
- ) => void;
16
-
17
- increaseBlockIndent: () => void;
18
- decreaseBlockIndent: () => void;
19
-
20
- updateBlock: (updatedBlock: PartialBlock) => void;
5
+ editor: BlockNoteEditor;
21
6
  };
22
7
 
23
8
  export type FormattingToolbarDynamicParams = {
24
- boldIsActive: boolean;
25
- italicIsActive: boolean;
26
- underlineIsActive: boolean;
27
- strikeIsActive: boolean;
28
- hyperlinkIsActive: boolean;
29
- activeHyperlinkUrl: string;
30
- activeHyperlinkText: string;
31
-
32
- textColor: string;
33
- backgroundColor: string;
34
- textAlignment: "left" | "center" | "right" | "justify";
35
-
36
- canIncreaseBlockIndent: boolean;
37
- canDecreaseBlockIndent: boolean;
38
-
39
- block: Block;
40
-
41
9
  referenceRect: DOMRect;
42
10
  };
43
11