@blocknote/core 0.13.4 → 0.14.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 (51) hide show
  1. package/dist/blocknote.js +526 -477
  2. package/dist/blocknote.js.map +1 -1
  3. package/dist/blocknote.umd.cjs +5 -5
  4. package/dist/blocknote.umd.cjs.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/dist/webpack-stats.json +1 -1
  7. package/package.json +29 -29
  8. package/src/api/blockManipulation/__snapshots__/blockManipulation.test.ts.snap +98 -0
  9. package/src/api/blockManipulation/blockManipulation.test.ts +86 -7
  10. package/src/api/exporters/html/__snapshots__/customBlock/basic/external.html +1 -0
  11. package/src/api/exporters/html/__snapshots__/customBlock/basic/internal.html +1 -0
  12. package/src/api/exporters/html/__snapshots__/customParagraph/lineBreaks/external.html +1 -0
  13. package/src/api/exporters/html/__snapshots__/customParagraph/lineBreaks/internal.html +1 -0
  14. package/src/api/exporters/html/__snapshots__/paragraph/lineBreaks/external.html +1 -0
  15. package/src/api/exporters/html/__snapshots__/paragraph/lineBreaks/internal.html +1 -0
  16. package/src/api/exporters/markdown/__snapshots__/customBlock/basic/markdown.md +5 -0
  17. package/src/api/exporters/markdown/__snapshots__/customParagraph/lineBreaks/markdown.md +1 -0
  18. package/src/api/exporters/markdown/__snapshots__/paragraph/lineBreaks/markdown.md +2 -0
  19. package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +566 -0
  20. package/src/api/nodeConversions/nodeConversions.test.ts +2 -0
  21. package/src/api/nodeConversions/nodeConversions.ts +2 -4
  22. package/src/api/parsers/html/__snapshots__/paste/parse-image-in-paragraph.json +16 -0
  23. package/src/api/parsers/html/parseHTML.test.ts +8 -0
  24. package/src/api/parsers/pasteExtension.ts +5 -0
  25. package/src/api/testUtil/cases/customBlocks.ts +9 -0
  26. package/src/api/testUtil/cases/defaultSchema.ts +9 -0
  27. package/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts +6 -6
  28. package/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts +7 -2
  29. package/src/blocks/TableBlockContent/TableBlockContent.ts +23 -1
  30. package/src/editor/Block.css +2 -3
  31. package/src/editor/BlockNoteEditor.ts +7 -6
  32. package/src/editor/BlockNoteExtensions.ts +10 -1
  33. package/src/editor/BlockNoteTipTapEditor.ts +1 -0
  34. package/src/extensions/FilePanel/FilePanelPlugin.ts +16 -12
  35. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +12 -15
  36. package/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +6 -2
  37. package/src/extensions/Placeholder/PlaceholderPlugin.ts +5 -1
  38. package/src/extensions/SideMenu/SideMenuPlugin.ts +157 -118
  39. package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +5 -2
  40. package/src/extensions/TableHandles/TableHandlesPlugin.ts +7 -4
  41. package/src/i18n/locales/pt.ts +1 -1
  42. package/src/i18n/locales/zh.ts +1 -1
  43. package/src/pm-nodes/BlockContainer.ts +11 -7
  44. package/src/schema/blocks/createSpec.ts +2 -2
  45. package/src/schema/inlineContent/createSpec.ts +2 -2
  46. package/types/src/editor/BlockNoteEditor.d.ts +1 -1
  47. package/types/src/extensions/FilePanel/FilePanelPlugin.d.ts +5 -5
  48. package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +0 -1
  49. package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +9 -8
  50. package/types/src/schema/blocks/createSpec.d.ts +2 -2
  51. package/types/src/schema/inlineContent/createSpec.d.ts +2 -2
@@ -2,7 +2,7 @@ import { Editor } from "@tiptap/core";
2
2
  import { getBlockInfoFromPos } from "../../api/getBlockInfoFromPos";
3
3
 
4
4
  export const handleEnter = (editor: Editor) => {
5
- const { node, contentType } = getBlockInfoFromPos(
5
+ const { contentNode, contentType } = getBlockInfoFromPos(
6
6
  editor.state.doc,
7
7
  editor.state.selection.from
8
8
  )!;
@@ -23,9 +23,9 @@ export const handleEnter = (editor: Editor) => {
23
23
 
24
24
  return editor.commands.first(({ state, chain, commands }) => [
25
25
  () =>
26
- // Changes list item block to a text block if the content is empty.
26
+ // Changes list item block to a paragraph block if the content is empty.
27
27
  commands.command(() => {
28
- if (node.textContent.length === 0) {
28
+ if (contentNode.childCount === 0) {
29
29
  return commands.BNUpdateBlock(state.selection.from, {
30
30
  type: "paragraph",
31
31
  props: {},
@@ -36,10 +36,10 @@ export const handleEnter = (editor: Editor) => {
36
36
  }),
37
37
 
38
38
  () =>
39
- // Splits the current block, moving content inside that's after the cursor to a new block of the same type
40
- // below.
39
+ // Splits the current block, moving content inside that's after the cursor
40
+ // to a new block of the same type below.
41
41
  commands.command(() => {
42
- if (node.textContent.length > 0) {
42
+ if (contentNode.childCount > 0) {
43
43
  chain()
44
44
  .deleteSelection()
45
45
  .BNSplitBlock(state.selection.from, true)
@@ -4,7 +4,6 @@ import {
4
4
  } from "../../schema";
5
5
  import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers";
6
6
  import { defaultProps } from "../defaultProps";
7
- import { handleEnter } from "../ListItemBlockContent/ListItemKeyboardShortcuts";
8
7
  import { getCurrentBlockContentType } from "../../api/getCurrentBlockContentType";
9
8
 
10
9
  export const paragraphPropSchema = {
@@ -18,7 +17,6 @@ export const ParagraphBlockContent = createStronglyTypedTiptapNode({
18
17
 
19
18
  addKeyboardShortcuts() {
20
19
  return {
21
- Enter: () => handleEnter(this.editor),
22
20
  "Mod-Alt-0": () => {
23
21
  if (getCurrentBlockContentType(this.editor) !== "inline*") {
24
22
  return true;
@@ -41,6 +39,13 @@ export const ParagraphBlockContent = createStronglyTypedTiptapNode({
41
39
  {
42
40
  tag: "p",
43
41
  priority: 200,
42
+ getAttrs: (element) => {
43
+ if (typeof element === "string" || !element.textContent?.trim()) {
44
+ return false;
45
+ }
46
+
47
+ return {};
48
+ },
44
49
  node: "paragraph",
45
50
  },
46
51
  ];
@@ -45,7 +45,29 @@ const TableParagraph = Node.create({
45
45
  content: "inline*",
46
46
 
47
47
  parseHTML() {
48
- return [{ tag: "p" }];
48
+ return [
49
+ { tag: "td" },
50
+ {
51
+ tag: "p",
52
+ getAttrs: (element) => {
53
+ if (typeof element === "string" || !element.textContent) {
54
+ return false;
55
+ }
56
+
57
+ const parent = element.parentElement;
58
+
59
+ if (parent === null) {
60
+ return false;
61
+ }
62
+
63
+ if (parent.tagName === "TD") {
64
+ return {};
65
+ }
66
+
67
+ return false;
68
+ },
69
+ },
70
+ ];
49
71
  },
50
72
 
51
73
  renderHTML({ HTMLAttributes }) {
@@ -279,7 +279,6 @@ NESTED BLOCKS
279
279
  background-color: rgb(242, 241, 238);
280
280
  border-radius: 4px;
281
281
  color: rgb(125, 121, 122);
282
- cursor: pointer;
283
282
  display: flex;
284
283
  flex-direction: row;
285
284
  gap: 10px;
@@ -287,7 +286,7 @@ NESTED BLOCKS
287
286
  width: 100%;
288
287
  }
289
288
 
290
- [data-file-block] .bn-add-file-button:hover {
289
+ .bn-editor[contenteditable="true"] [data-file-block] .bn-add-file-button:hover {
291
290
  background-color: rgb(225, 225, 225);
292
291
  }
293
292
 
@@ -363,7 +362,7 @@ NESTED BLOCKS
363
362
  }
364
363
 
365
364
  /* PLACEHOLDERS*/
366
- .bn-inline-content:has(> .ProseMirror-trailingBreak):before {
365
+ .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):before {
367
366
  /*float: left; */
368
367
  pointer-events: none;
369
368
  height: 0;
@@ -194,11 +194,7 @@ export class BlockNoteEditor<
194
194
  ISchema,
195
195
  SSchema
196
196
  >;
197
- public readonly filePanel?: FilePanelProsemirrorPlugin<
198
- BSchema,
199
- ISchema,
200
- SSchema
201
- >;
197
+ public readonly filePanel?: FilePanelProsemirrorPlugin<ISchema, SSchema>;
202
198
  public readonly tableHandles?: TableHandlesProsemirrorPlugin<
203
199
  ISchema,
204
200
  SSchema
@@ -307,6 +303,7 @@ export class BlockNoteEditor<
307
303
  this.resolveFileUrl = newOptions.resolveFileUrl || (async (url) => url);
308
304
 
309
305
  if (newOptions.collaboration && newOptions.initialContent) {
306
+ // eslint-disable-next-line no-console
310
307
  console.warn(
311
308
  "When using Collaboration, initialContent might cause conflicts, because changes should come from the collaboration provider"
312
309
  );
@@ -777,7 +774,11 @@ export class BlockNoteEditor<
777
774
  for (const mark of marks) {
778
775
  const config = this.schema.styleSchema[mark.type.name];
779
776
  if (!config) {
780
- console.warn("mark not found in styleschema", mark.type.name);
777
+ if (mark.type.name !== "link") {
778
+ // eslint-disable-next-line no-console
779
+ console.warn("mark not found in styleschema", mark.type.name);
780
+ }
781
+
781
782
  continue;
782
783
  }
783
784
  if (config.propSchema === "boolean") {
@@ -75,7 +75,16 @@ export const getBlockNoteExtensions = <
75
75
  Text,
76
76
 
77
77
  // marks:
78
- Link,
78
+ Link.extend({
79
+ addKeyboardShortcuts() {
80
+ return {
81
+ "Mod-k": () => {
82
+ this.editor.commands.toggleLink({ href: "" });
83
+ return true;
84
+ },
85
+ };
86
+ },
87
+ }),
79
88
  ...Object.values(opts.styleSpecs).map((styleSpec) => {
80
89
  return styleSpec.implementation.mark;
81
90
  }),
@@ -87,6 +87,7 @@ export class BlockNoteTipTapEditor extends TiptapEditor {
87
87
  this.options.parseOptions
88
88
  );
89
89
  } catch (e) {
90
+ // eslint-disable-next-line no-console
90
91
  console.error(
91
92
  "Error creating document from blocks passed as `initialContent`. Caused by exception: ",
92
93
  e
@@ -5,7 +5,6 @@ import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
5
5
  import { UiElementPosition } from "../../extensions-shared/UiElementPosition";
6
6
  import type {
7
7
  BlockFromConfig,
8
- BlockSchema,
9
8
  FileBlockConfig,
10
9
  InlineContentSchema,
11
10
  StyleSchema,
@@ -26,9 +25,12 @@ export class FilePanelView<I extends InlineContentSchema, S extends StyleSchema>
26
25
  public state?: FilePanelState<I, S>;
27
26
  public emitUpdate: () => void;
28
27
 
29
- public prevWasEditable: boolean | null = null;
30
-
31
28
  constructor(
29
+ private readonly editor: BlockNoteEditor<
30
+ Record<string, FileBlockConfig>,
31
+ I,
32
+ S
33
+ >,
32
34
  private readonly pluginKey: PluginKey,
33
35
  private readonly pmView: EditorView,
34
36
  emitUpdate: (state: FilePanelState<I, S>) => void
@@ -42,10 +44,12 @@ export class FilePanelView<I extends InlineContentSchema, S extends StyleSchema>
42
44
  };
43
45
 
44
46
  pmView.dom.addEventListener("mousedown", this.mouseDownHandler);
45
-
46
47
  pmView.dom.addEventListener("dragstart", this.dragstartHandler);
47
48
 
48
- document.addEventListener("scroll", this.scrollHandler);
49
+ // Setting capture=true ensures that any parent container of the editor that
50
+ // gets scrolled will trigger the scroll event. Scroll events do not bubble
51
+ // and so won't propagate to the document by default.
52
+ document.addEventListener("scroll", this.scrollHandler, true);
49
53
  }
50
54
 
51
55
  mouseDownHandler = () => {
@@ -79,7 +83,7 @@ export class FilePanelView<I extends InlineContentSchema, S extends StyleSchema>
79
83
  block: BlockFromConfig<FileBlockConfig, I, S>;
80
84
  } = this.pluginKey.getState(view.state);
81
85
 
82
- if (!this.state?.show && pluginState.block) {
86
+ if (!this.state?.show && pluginState.block && this.editor.isEditable) {
83
87
  const blockElement = document.querySelector(
84
88
  `[data-node-type="blockContainer"][data-id="${pluginState.block.id}"]`
85
89
  )!;
@@ -97,7 +101,8 @@ export class FilePanelView<I extends InlineContentSchema, S extends StyleSchema>
97
101
 
98
102
  if (
99
103
  !view.state.selection.eq(prevState.selection) ||
100
- !view.state.doc.eq(prevState.doc)
104
+ !view.state.doc.eq(prevState.doc) ||
105
+ !this.editor.isEditable
101
106
  ) {
102
107
  if (this.state?.show) {
103
108
  this.state.show = false;
@@ -119,29 +124,28 @@ export class FilePanelView<I extends InlineContentSchema, S extends StyleSchema>
119
124
 
120
125
  this.pmView.dom.removeEventListener("dragstart", this.dragstartHandler);
121
126
 
122
- document.removeEventListener("scroll", this.scrollHandler);
127
+ document.removeEventListener("scroll", this.scrollHandler, true);
123
128
  }
124
129
  }
125
130
 
126
131
  const filePanelPluginKey = new PluginKey("FilePanelPlugin");
127
132
 
128
133
  export class FilePanelProsemirrorPlugin<
129
- B extends BlockSchema,
130
134
  I extends InlineContentSchema,
131
135
  S extends StyleSchema
132
136
  > extends EventEmitter<any> {
133
137
  private view: FilePanelView<I, S> | undefined;
134
138
  public readonly plugin: Plugin;
135
139
 
136
- constructor(_editor: BlockNoteEditor<B, I, S>) {
140
+ constructor(editor: BlockNoteEditor<Record<string, FileBlockConfig>, I, S>) {
137
141
  super();
138
142
  this.plugin = new Plugin<{
139
143
  block: BlockFromConfig<FileBlockConfig, I, S> | undefined;
140
144
  }>({
141
145
  key: filePanelPluginKey,
142
146
  view: (editorView) => {
143
- this.view = new FilePanelView(
144
- // editor,
147
+ this.view = new FilePanelView<I, S>(
148
+ editor,
145
149
  filePanelPluginKey,
146
150
  editorView,
147
151
  (state) => {
@@ -15,7 +15,6 @@ export class FormattingToolbarView implements PluginView {
15
15
 
16
16
  public preventHide = false;
17
17
  public preventShow = false;
18
- public prevWasEditable: boolean | null = null;
19
18
 
20
19
  public shouldShow: (props: {
21
20
  view: EditorView;
@@ -60,7 +59,10 @@ export class FormattingToolbarView implements PluginView {
60
59
  pmView.dom.addEventListener("dragstart", this.dragHandler);
61
60
  pmView.dom.addEventListener("dragover", this.dragHandler);
62
61
 
63
- document.addEventListener("scroll", this.scrollHandler);
62
+ // Setting capture=true ensures that any parent container of the editor that
63
+ // gets scrolled will trigger the scroll event. Scroll events do not bubble
64
+ // and so won't propagate to the document by default.
65
+ document.addEventListener("scroll", this.scrollHandler, true);
64
66
  }
65
67
 
66
68
  viewMousedownHandler = () => {
@@ -93,16 +95,10 @@ export class FormattingToolbarView implements PluginView {
93
95
  const isSame =
94
96
  oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection);
95
97
 
96
- if (
97
- (this.prevWasEditable === null ||
98
- this.prevWasEditable === this.editor.isEditable) &&
99
- (composing || isSame)
100
- ) {
98
+ if (composing || isSame) {
101
99
  return;
102
100
  }
103
101
 
104
- this.prevWasEditable = this.editor.isEditable;
105
-
106
102
  // support for CellSelections
107
103
  const { ranges } = selection;
108
104
  const from = Math.min(...ranges.map((range) => range.$from.pos));
@@ -116,11 +112,12 @@ export class FormattingToolbarView implements PluginView {
116
112
  });
117
113
 
118
114
  // Checks if menu should be shown/updated.
119
- if (
120
- this.editor.isEditable &&
121
- !this.preventShow &&
122
- (shouldShow || this.preventHide)
123
- ) {
115
+ if (!this.preventShow && (shouldShow || this.preventHide)) {
116
+ // Unlike other UI elements, we don't prevent the formatting toolbar from
117
+ // showing when the editor is not editable. This is because some buttons,
118
+ // e.g. the download file button, should still be accessible. Therefore,
119
+ // logic for hiding when the editor is non-editable is handled
120
+ // individually in each button.
124
121
  this.state = {
125
122
  show: true,
126
123
  referencePos: this.getSelectionBoundingBox(),
@@ -150,7 +147,7 @@ export class FormattingToolbarView implements PluginView {
150
147
  this.pmView.dom.removeEventListener("dragstart", this.dragHandler);
151
148
  this.pmView.dom.removeEventListener("dragover", this.dragHandler);
152
149
 
153
- document.removeEventListener("scroll", this.scrollHandler);
150
+ document.removeEventListener("scroll", this.scrollHandler, true);
154
151
  }
155
152
 
156
153
  closeMenu = () => {
@@ -62,7 +62,11 @@ class LinkToolbarView implements PluginView {
62
62
 
63
63
  this.pmView.dom.addEventListener("mouseover", this.mouseOverHandler);
64
64
  document.addEventListener("click", this.clickHandler, true);
65
- document.addEventListener("scroll", this.scrollHandler);
65
+
66
+ // Setting capture=true ensures that any parent container of the editor that
67
+ // gets scrolled will trigger the scroll event. Scroll events do not bubble
68
+ // and so won't propagate to the document by default.
69
+ document.addEventListener("scroll", this.scrollHandler, true);
66
70
  }
67
71
 
68
72
  mouseOverHandler = (event: MouseEvent) => {
@@ -267,7 +271,7 @@ class LinkToolbarView implements PluginView {
267
271
 
268
272
  destroy() {
269
273
  this.pmView.dom.removeEventListener("mouseover", this.mouseOverHandler);
270
- document.removeEventListener("scroll", this.scrollHandler);
274
+ document.removeEventListener("scroll", this.scrollHandler, true);
271
275
  document.removeEventListener("click", this.clickHandler, true);
272
276
  }
273
277
  }
@@ -12,11 +12,15 @@ export const PlaceholderPlugin = (
12
12
  key: PLUGIN_KEY,
13
13
  view: () => {
14
14
  const styleEl = document.createElement("style");
15
+ const nonce = editor._tiptapEditor.options.injectNonce;
16
+ if (nonce) {
17
+ styleEl.setAttribute("nonce", nonce);
18
+ }
15
19
  document.head.appendChild(styleEl);
16
20
  const styleSheet = styleEl.sheet!;
17
21
 
18
22
  const getBaseSelector = (additionalSelectors = "") =>
19
- `.bn-block-content${additionalSelectors} .bn-inline-content:has(> .ProseMirror-trailingBreak):before`;
23
+ `.bn-block-content${additionalSelectors} .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):before`;
20
24
 
21
25
  const getSelector = (
22
26
  blockType: string | "default",