@blocknote/core 0.11.1 → 0.12.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 (129) hide show
  1. package/README.md +13 -17
  2. package/dist/blocknote.js +1611 -1408
  3. package/dist/blocknote.js.map +1 -1
  4. package/dist/blocknote.umd.cjs +6 -6
  5. package/dist/blocknote.umd.cjs.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/dist/webpack-stats.json +1 -1
  8. package/package.json +8 -4
  9. package/src/api/blockManipulation/blockManipulation.test.ts +19 -15
  10. package/src/api/blockManipulation/blockManipulation.ts +107 -17
  11. package/src/api/exporters/html/externalHTMLExporter.ts +3 -7
  12. package/src/api/exporters/html/htmlConversion.test.ts +6 -3
  13. package/src/api/exporters/html/internalHTMLSerializer.ts +3 -7
  14. package/src/api/exporters/html/util/sharedHTMLConversion.ts +3 -3
  15. package/src/api/exporters/markdown/markdownExporter.test.ts +7 -3
  16. package/src/api/exporters/markdown/markdownExporter.ts +2 -6
  17. package/src/api/nodeConversions/nodeConversions.test.ts +14 -7
  18. package/src/api/nodeConversions/nodeConversions.ts +1 -2
  19. package/src/api/parsers/html/parseHTML.test.ts +5 -1
  20. package/src/api/parsers/html/parseHTML.ts +2 -6
  21. package/src/api/parsers/html/util/nestedLists.ts +11 -1
  22. package/src/api/parsers/markdown/parseMarkdown.test.ts +3 -0
  23. package/src/api/parsers/markdown/parseMarkdown.ts +2 -6
  24. package/src/api/testUtil/cases/customBlocks.ts +18 -16
  25. package/src/api/testUtil/cases/customInlineContent.ts +12 -13
  26. package/src/api/testUtil/cases/customStyles.ts +12 -10
  27. package/src/api/testUtil/index.ts +4 -2
  28. package/src/api/testUtil/partialBlockTestUtil.ts +2 -6
  29. package/src/blocks/ImageBlockContent/ImageBlockContent.ts +1 -2
  30. package/src/blocks/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.ts +8 -1
  31. package/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts +13 -0
  32. package/src/blocks/defaultBlockHelpers.ts +3 -3
  33. package/src/blocks/defaultBlockTypeGuards.ts +84 -0
  34. package/src/blocks/defaultBlocks.ts +29 -3
  35. package/src/editor/Block.css +2 -31
  36. package/src/editor/BlockNoteEditor.ts +219 -263
  37. package/src/editor/BlockNoteExtensions.ts +5 -2
  38. package/src/editor/BlockNoteSchema.ts +98 -0
  39. package/src/editor/BlockNoteTipTapEditor.ts +162 -0
  40. package/src/editor/cursorPositionTypes.ts +2 -6
  41. package/src/editor/editor.css +0 -1
  42. package/src/editor/selectionTypes.ts +2 -6
  43. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +22 -29
  44. package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +26 -27
  45. package/src/extensions/ImageToolbar/ImageToolbarPlugin.ts +45 -51
  46. package/src/extensions/Placeholder/PlaceholderExtension.ts +81 -88
  47. package/src/extensions/SideMenu/SideMenuPlugin.ts +55 -56
  48. package/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts +8 -0
  49. package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +353 -0
  50. package/src/extensions/{SlashMenu/defaultSlashMenuItems.ts → SuggestionMenu/getDefaultSlashMenuItems.ts} +119 -89
  51. package/src/extensions/TableHandles/TableHandlesPlugin.ts +62 -45
  52. package/src/extensions-shared/UiElementPosition.ts +4 -0
  53. package/src/index.ts +6 -6
  54. package/src/pm-nodes/BlockContainer.ts +5 -9
  55. package/src/schema/blocks/types.ts +15 -15
  56. package/src/schema/inlineContent/createSpec.ts +2 -2
  57. package/src/schema/inlineContent/types.ts +1 -1
  58. package/src/util/browser.ts +6 -4
  59. package/src/util/typescript.ts +7 -4
  60. package/types/src/api/blockManipulation/blockManipulation.d.ts +6 -1
  61. package/types/src/api/exporters/html/externalHTMLExporter.d.ts +2 -1
  62. package/types/src/api/exporters/html/internalHTMLSerializer.d.ts +2 -1
  63. package/types/src/api/exporters/markdown/markdownExporter.d.ts +2 -1
  64. package/types/src/api/nodeConversions/nodeConversions.d.ts +2 -1
  65. package/types/src/api/parsers/html/parseHTML.d.ts +2 -1
  66. package/types/src/api/parsers/markdown/parseMarkdown.d.ts +2 -1
  67. package/types/src/api/testUtil/cases/customBlocks.d.ts +72 -13
  68. package/types/src/api/testUtil/cases/customInlineContent.d.ts +281 -6
  69. package/types/src/api/testUtil/cases/customStyles.d.ts +247 -13
  70. package/types/src/api/testUtil/index.d.ts +4 -2
  71. package/types/src/api/testUtil/partialBlockTestUtil.d.ts +2 -1
  72. package/types/src/blocks/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.d.ts +6 -1
  73. package/types/src/blocks/defaultBlockHelpers.d.ts +2 -2
  74. package/types/src/blocks/defaultBlockTypeGuards.d.ts +24 -0
  75. package/types/src/blocks/defaultBlocks.d.ts +21 -15
  76. package/types/src/editor/BlockNoteEditor.d.ts +48 -53
  77. package/types/src/editor/BlockNoteExtensions.d.ts +1 -0
  78. package/types/src/editor/BlockNoteSchema.d.ts +34 -0
  79. package/types/src/editor/BlockNoteTipTapEditor.d.ts +28 -0
  80. package/types/src/editor/cursorPositionTypes.d.ts +2 -1
  81. package/types/src/editor/selectionTypes.d.ts +2 -1
  82. package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +5 -6
  83. package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.d.ts +2 -2
  84. package/types/src/extensions/ImageToolbar/ImageToolbarPlugin.d.ts +15 -14
  85. package/types/src/extensions/Placeholder/PlaceholderExtension.d.ts +2 -15
  86. package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +8 -7
  87. package/types/src/extensions/SuggestionMenu/DefaultSuggestionItem.d.ts +8 -0
  88. package/types/src/extensions/SuggestionMenu/SuggestionPlugin.d.ts +31 -0
  89. package/types/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.d.ts +10 -0
  90. package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +7 -7
  91. package/types/src/extensions-shared/UiElementPosition.d.ts +4 -0
  92. package/types/src/index.d.ts +6 -6
  93. package/types/src/pm-nodes/BlockContainer.d.ts +3 -2
  94. package/types/src/pm-nodes/BlockGroup.d.ts +1 -1
  95. package/types/src/schema/blocks/types.d.ts +15 -15
  96. package/types/src/schema/inlineContent/types.d.ts +1 -1
  97. package/types/src/util/browser.d.ts +1 -0
  98. package/types/src/util/typescript.d.ts +1 -0
  99. package/src/extensions/SlashMenu/BaseSlashMenuItem.ts +0 -12
  100. package/src/extensions/SlashMenu/SlashMenuPlugin.ts +0 -53
  101. package/src/extensions-shared/BaseUiElementTypes.ts +0 -8
  102. package/src/extensions-shared/README.md +0 -3
  103. package/src/extensions-shared/suggestion/SuggestionItem.ts +0 -3
  104. package/src/extensions-shared/suggestion/SuggestionPlugin.ts +0 -448
  105. package/types/src/extensions/SlashMenu/BaseSlashMenuItem.d.ts +0 -7
  106. package/types/src/extensions/SlashMenu/SlashMenuPlugin.d.ts +0 -13
  107. package/types/src/extensions/SlashMenu/defaultSlashMenuItems.d.ts +0 -3
  108. package/types/src/extensions-shared/BaseUiElementTypes.d.ts +0 -7
  109. package/types/src/extensions-shared/suggestion/SuggestionItem.d.ts +0 -3
  110. package/types/src/extensions-shared/suggestion/SuggestionPlugin.d.ts +0 -36
  111. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-100.woff +0 -0
  112. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-100.woff2 +0 -0
  113. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-200.woff +0 -0
  114. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-200.woff2 +0 -0
  115. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-300.woff +0 -0
  116. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-300.woff2 +0 -0
  117. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-500.woff +0 -0
  118. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-500.woff2 +0 -0
  119. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-600.woff +0 -0
  120. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-600.woff2 +0 -0
  121. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-700.woff +0 -0
  122. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-700.woff2 +0 -0
  123. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-800.woff +0 -0
  124. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-800.woff2 +0 -0
  125. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-900.woff +0 -0
  126. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-900.woff2 +0 -0
  127. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-regular.woff +0 -0
  128. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-regular.woff2 +0 -0
  129. /package/src/{assets/fonts-inter.css → fonts/inter.css} +0 -0
@@ -1,51 +1,44 @@
1
1
  import { EditorState, Plugin, PluginKey } from "prosemirror-state";
2
2
  import { EditorView } from "prosemirror-view";
3
3
 
4
- import { EventEmitter } from "../../util/EventEmitter";
5
4
  import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
6
- import {
7
- BlockSchema,
5
+ import type {
6
+ BlockFromConfig,
8
7
  InlineContentSchema,
9
- SpecificBlock,
10
8
  StyleSchema,
11
9
  } from "../../schema";
12
- import {
13
- BaseUiElementCallbacks,
14
- BaseUiElementState,
15
- } from "../../extensions-shared/BaseUiElementTypes";
16
- export type ImageToolbarCallbacks = BaseUiElementCallbacks;
10
+ import { UiElementPosition } from "../../extensions-shared/UiElementPosition";
11
+ import { EventEmitter } from "../../util/EventEmitter";
12
+ import { DefaultBlockSchema } from "../../blocks/defaultBlocks";
17
13
 
18
14
  export type ImageToolbarState<
19
- B extends BlockSchema,
20
15
  I extends InlineContentSchema,
21
- S extends StyleSchema = StyleSchema
22
- > = BaseUiElementState & {
23
- block: SpecificBlock<B, "image", I, S>;
16
+ S extends StyleSchema
17
+ > = UiElementPosition & {
18
+ // TODO: This typing is not quite right (children should be from BSchema)
19
+ block: BlockFromConfig<DefaultBlockSchema["image"], I, S>;
24
20
  };
25
21
 
26
22
  export class ImageToolbarView<
27
- BSchema extends BlockSchema,
28
23
  I extends InlineContentSchema,
29
24
  S extends StyleSchema
30
25
  > {
31
- private imageToolbarState?: ImageToolbarState<BSchema, I, S>;
32
- public updateImageToolbar: () => void;
26
+ public state?: ImageToolbarState<I, S>;
27
+ public emitUpdate: () => void;
33
28
 
34
29
  public prevWasEditable: boolean | null = null;
35
30
 
36
31
  constructor(
37
32
  private readonly pluginKey: PluginKey,
38
33
  private readonly pmView: EditorView,
39
- updateImageToolbar: (
40
- imageToolbarState: ImageToolbarState<BSchema, I, S>
41
- ) => void
34
+ emitUpdate: (state: ImageToolbarState<I, S>) => void
42
35
  ) {
43
- this.updateImageToolbar = () => {
44
- if (!this.imageToolbarState) {
36
+ this.emitUpdate = () => {
37
+ if (!this.state) {
45
38
  throw new Error("Attempting to update uninitialized image toolbar");
46
39
  }
47
40
 
48
- updateImageToolbar(this.imageToolbarState);
41
+ emitUpdate(this.state);
49
42
  };
50
43
 
51
44
  pmView.dom.addEventListener("mousedown", this.mouseDownHandler);
@@ -58,17 +51,17 @@ export class ImageToolbarView<
58
51
  }
59
52
 
60
53
  mouseDownHandler = () => {
61
- if (this.imageToolbarState?.show) {
62
- this.imageToolbarState.show = false;
63
- this.updateImageToolbar();
54
+ if (this.state?.show) {
55
+ this.state.show = false;
56
+ this.emitUpdate();
64
57
  }
65
58
  };
66
59
 
67
60
  // For dragging the whole editor.
68
61
  dragstartHandler = () => {
69
- if (this.imageToolbarState?.show) {
70
- this.imageToolbarState.show = false;
71
- this.updateImageToolbar();
62
+ if (this.state?.show) {
63
+ this.state.show = false;
64
+ this.emitUpdate();
72
65
  }
73
66
  };
74
67
 
@@ -88,41 +81,40 @@ export class ImageToolbarView<
88
81
  return;
89
82
  }
90
83
 
91
- if (this.imageToolbarState?.show) {
92
- this.imageToolbarState.show = false;
93
- this.updateImageToolbar();
84
+ if (this.state?.show) {
85
+ this.state.show = false;
86
+ this.emitUpdate();
94
87
  }
95
88
  };
96
89
 
97
90
  scrollHandler = () => {
98
- if (this.imageToolbarState?.show) {
91
+ if (this.state?.show) {
99
92
  const blockElement = document.querySelector(
100
- `[data-node-type="blockContainer"][data-id="${this.imageToolbarState.block.id}"]`
93
+ `[data-node-type="blockContainer"][data-id="${this.state.block.id}"]`
101
94
  )!;
102
95
 
103
- this.imageToolbarState.referencePos =
104
- blockElement.getBoundingClientRect();
105
- this.updateImageToolbar();
96
+ this.state.referencePos = blockElement.getBoundingClientRect();
97
+ this.emitUpdate();
106
98
  }
107
99
  };
108
100
 
109
101
  update(view: EditorView, prevState: EditorState) {
110
102
  const pluginState: {
111
- block: SpecificBlock<BSchema, "image", I, S>;
103
+ block: BlockFromConfig<DefaultBlockSchema["image"], I, S>;
112
104
  } = this.pluginKey.getState(view.state);
113
105
 
114
- if (!this.imageToolbarState?.show && pluginState.block) {
106
+ if (!this.state?.show && pluginState.block) {
115
107
  const blockElement = document.querySelector(
116
108
  `[data-node-type="blockContainer"][data-id="${pluginState.block.id}"]`
117
109
  )!;
118
110
 
119
- this.imageToolbarState = {
111
+ this.state = {
120
112
  show: true,
121
113
  referencePos: blockElement.getBoundingClientRect(),
122
114
  block: pluginState.block,
123
115
  };
124
116
 
125
- this.updateImageToolbar();
117
+ this.emitUpdate();
126
118
 
127
119
  return;
128
120
  }
@@ -131,10 +123,10 @@ export class ImageToolbarView<
131
123
  !view.state.selection.eq(prevState.selection) ||
132
124
  !view.state.doc.eq(prevState.doc)
133
125
  ) {
134
- if (this.imageToolbarState?.show) {
135
- this.imageToolbarState.show = false;
126
+ if (this.state?.show) {
127
+ this.state.show = false;
136
128
 
137
- this.updateImageToolbar();
129
+ this.emitUpdate();
138
130
  }
139
131
  }
140
132
  }
@@ -150,20 +142,21 @@ export class ImageToolbarView<
150
142
  }
151
143
  }
152
144
 
153
- export const imageToolbarPluginKey = new PluginKey("ImageToolbarPlugin");
145
+ const imageToolbarPluginKey = new PluginKey("ImageToolbarPlugin");
154
146
 
155
147
  export class ImageToolbarProsemirrorPlugin<
156
- BSchema extends BlockSchema,
157
148
  I extends InlineContentSchema,
158
149
  S extends StyleSchema
159
150
  > extends EventEmitter<any> {
160
- private view: ImageToolbarView<BSchema, I, S> | undefined;
151
+ private view: ImageToolbarView<I, S> | undefined;
161
152
  public readonly plugin: Plugin;
162
153
 
163
- constructor(_editor: BlockNoteEditor<BSchema, I, S>) {
154
+ constructor(
155
+ _editor: BlockNoteEditor<{ image: DefaultBlockSchema["image"] }, I, S>
156
+ ) {
164
157
  super();
165
158
  this.plugin = new Plugin<{
166
- block: SpecificBlock<BSchema, "image", I, S> | undefined;
159
+ block: BlockFromConfig<DefaultBlockSchema["image"], I, S> | undefined;
167
160
  }>({
168
161
  key: imageToolbarPluginKey,
169
162
  view: (editorView) => {
@@ -184,8 +177,9 @@ export class ImageToolbarProsemirrorPlugin<
184
177
  };
185
178
  },
186
179
  apply: (transaction) => {
187
- const block: SpecificBlock<BSchema, "image", I, S> | undefined =
188
- transaction.getMeta(imageToolbarPluginKey)?.block;
180
+ const block:
181
+ | BlockFromConfig<DefaultBlockSchema["image"], I, S>
182
+ | undefined = transaction.getMeta(imageToolbarPluginKey)?.block;
189
183
 
190
184
  return {
191
185
  block,
@@ -195,7 +189,7 @@ export class ImageToolbarProsemirrorPlugin<
195
189
  });
196
190
  }
197
191
 
198
- public onUpdate(callback: (state: ImageToolbarState<BSchema, I, S>) => void) {
192
+ public onUpdate(callback: (state: ImageToolbarState<I, S>) => void) {
199
193
  return this.on("update", callback);
200
194
  }
201
195
  }
@@ -1,8 +1,6 @@
1
- import { Editor, Extension } from "@tiptap/core";
2
- import { Node as ProsemirrorNode } from "prosemirror-model";
1
+ import { Extension } from "@tiptap/core";
3
2
  import { Plugin, PluginKey } from "prosemirror-state";
4
3
  import { Decoration, DecorationSet } from "prosemirror-view";
5
- import { slashMenuPluginKey } from "../SlashMenu/SlashMenuPlugin";
6
4
 
7
5
  const PLUGIN_KEY = new PluginKey(`blocknote-placeholder`);
8
6
 
@@ -14,21 +12,7 @@ const PLUGIN_KEY = new PluginKey(`blocknote-placeholder`);
14
12
  *
15
13
  */
16
14
  export interface PlaceholderOptions {
17
- emptyEditorClass: string;
18
- emptyNodeClass: string;
19
- isFilterClass: string;
20
- hasAnchorClass: string;
21
- placeholder:
22
- | ((PlaceholderProps: {
23
- editor: Editor;
24
- node: ProsemirrorNode;
25
- pos: number;
26
- hasAnchor: boolean;
27
- }) => string)
28
- | string;
29
- showOnlyWhenEditable: boolean;
30
- showOnlyCurrent: boolean;
31
- includeChildren: boolean;
15
+ placeholders: Record<string | "default", string>;
32
16
  }
33
17
 
34
18
  export const Placeholder = Extension.create<PlaceholderOptions>({
@@ -36,93 +20,102 @@ export const Placeholder = Extension.create<PlaceholderOptions>({
36
20
 
37
21
  addOptions() {
38
22
  return {
39
- emptyEditorClass: "bn-is-editor-empty",
40
- emptyNodeClass: "bn-is-empty",
41
- isFilterClass: "bn-is-filter",
42
- hasAnchorClass: "bn-has-anchor",
43
- placeholder: "Write something …",
44
- showOnlyWhenEditable: true,
45
- showOnlyCurrent: true,
46
- includeChildren: false,
23
+ placeholders: {
24
+ default: "Enter text or type '/' for commands",
25
+ heading: "Heading",
26
+ bulletListItem: "List",
27
+ numberedListItem: "List",
28
+ },
47
29
  };
48
30
  },
49
31
 
50
32
  addProseMirrorPlugins() {
33
+ const placeholders = this.options.placeholders;
51
34
  return [
52
35
  new Plugin({
53
36
  key: PLUGIN_KEY,
37
+ view: () => {
38
+ const styleEl = document.createElement("style");
39
+ document.head.appendChild(styleEl);
40
+ const styleSheet = styleEl.sheet!;
41
+
42
+ const getBaseSelector = (additionalSelectors = "") =>
43
+ `.bn-block-content${additionalSelectors} .bn-inline-content:has(> .ProseMirror-trailingBreak):before`;
44
+
45
+ const getSelector = (
46
+ blockType: string | "default",
47
+ mustBeFocused = true
48
+ ) => {
49
+ const mustBeFocusedSelector = mustBeFocused
50
+ ? `[data-is-empty-and-focused]`
51
+ : ``;
52
+
53
+ if (blockType === "default") {
54
+ return getBaseSelector(mustBeFocusedSelector);
55
+ }
56
+
57
+ const blockTypeSelector = `[data-content-type="${blockType}"]`;
58
+ return getBaseSelector(mustBeFocusedSelector + blockTypeSelector);
59
+ };
60
+
61
+ for (const [blockType, placeholder] of Object.entries(placeholders)) {
62
+ const mustBeFocused = blockType === "default";
63
+
64
+ styleSheet.insertRule(
65
+ `${getSelector(
66
+ blockType,
67
+ mustBeFocused
68
+ )}{ content: ${JSON.stringify(placeholder)}; }`
69
+ );
70
+
71
+ // For some reason, the placeholders which show when the block is focused
72
+ // take priority over ones which show depending on block type, so we need
73
+ // to make sure the block specific ones are also used when the block is
74
+ // focused.
75
+ if (!mustBeFocused) {
76
+ styleSheet.insertRule(
77
+ `${getSelector(blockType, true)}{ content: ${JSON.stringify(
78
+ placeholder
79
+ )}; }`
80
+ );
81
+ }
82
+ }
83
+
84
+ return {
85
+ destroy: () => {
86
+ document.head.removeChild(styleEl);
87
+ },
88
+ };
89
+ },
54
90
  props: {
91
+ // TODO: maybe also add placeholder for empty document ("e.g.: start writing..")
55
92
  decorations: (state) => {
56
93
  const { doc, selection } = state;
57
- // Get state of slash menu
58
- const menuState = slashMenuPluginKey.getState(state);
59
- const active =
60
- this.editor.isEditable || !this.options.showOnlyWhenEditable;
61
- const { anchor } = selection;
62
- const decorations: Decoration[] = [];
94
+
95
+ const active = this.editor.isEditable;
63
96
 
64
97
  if (!active) {
65
98
  return;
66
99
  }
67
100
 
68
- doc.descendants((node, pos) => {
69
- const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize;
70
- const isEmpty = !node.isLeaf && !node.childCount;
71
-
72
- if ((hasAnchor || !this.options.showOnlyCurrent) && isEmpty) {
73
- const classes = [this.options.emptyNodeClass];
74
-
75
- // TODO: Doesn't work?
76
- if (this.editor.isEmpty) {
77
- classes.push(this.options.emptyEditorClass);
78
- }
79
-
80
- if (hasAnchor) {
81
- classes.push(this.options.hasAnchorClass);
82
- }
83
-
84
- // If slash menu is of drag type and active, show the filter placeholder
85
- if (menuState?.triggerCharacter === "" && menuState?.active) {
86
- classes.push(this.options.isFilterClass);
87
- }
88
- // using widget, didn't work (caret position bug)
89
- // const decoration = Decoration.widget(
90
- // pos + 1,
91
- // () => {
92
- // const el = document.createElement("span");
93
- // el.innerText = "hello";
94
- // return el;
95
- // },
96
- // { side: 0 }
97
-
98
- // Code that sets variables / classes
99
- // const ph =
100
- // typeof this.options.placeholder === "function"
101
- // ? this.options.placeholder({
102
- // editor: this.editor,
103
- // node,
104
- // pos,
105
- // hasAnchor,
106
- // })
107
- // : this.options.placeholder;
108
- // const decoration = Decoration.node(pos, pos + node.nodeSize, {
109
- // class: classes.join(" "),
110
- // style: `--placeholder:'${ph.replaceAll("'", "\\'")}';`,
111
- // "data-placeholder": ph,
112
- // });
113
-
114
- // Latest version, only set isEmpty and hasAnchor, rest is done via CSS
115
-
116
- const decoration = Decoration.node(pos, pos + node.nodeSize, {
117
- class: classes.join(" "),
118
- });
119
- decorations.push(decoration);
120
- }
121
-
122
- return this.options.includeChildren;
101
+ if (!selection.empty) {
102
+ return;
103
+ }
104
+
105
+ const $pos = selection.$anchor;
106
+ const node = $pos.parent;
107
+
108
+ if (node.content.size > 0) {
109
+ return null;
110
+ }
111
+
112
+ const before = $pos.before();
113
+
114
+ const dec = Decoration.node(before, before + node.nodeSize, {
115
+ "data-is-empty-and-focused": "true",
123
116
  });
124
117
 
125
- return DecorationSet.create(doc, decorations);
118
+ return DecorationSet.create(doc, [dec]);
126
119
  },
127
120
  },
128
121
  }),
@@ -2,20 +2,17 @@ import { PluginView } from "@tiptap/pm/state";
2
2
  import { Node } from "prosemirror-model";
3
3
  import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state";
4
4
  import { EditorView } from "prosemirror-view";
5
+
5
6
  import { createExternalHTMLExporter } from "../../api/exporters/html/externalHTMLExporter";
6
7
  import { createInternalHTMLSerializer } from "../../api/exporters/html/internalHTMLSerializer";
7
8
  import { cleanHTMLToMarkdown } from "../../api/exporters/markdown/markdownExporter";
8
9
  import { getBlockInfoFromPos } from "../../api/getBlockInfoFromPos";
10
+ import { Block } from "../../blocks/defaultBlocks";
9
11
  import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
10
- import { BaseUiElementState } from "../../extensions-shared/BaseUiElementTypes";
11
- import {
12
- Block,
13
- BlockSchema,
14
- InlineContentSchema,
15
- StyleSchema,
16
- } from "../../schema";
12
+ import { UiElementPosition } from "../../extensions-shared/UiElementPosition";
13
+ import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
17
14
  import { EventEmitter } from "../../util/EventEmitter";
18
- import { slashMenuPluginKey } from "../SlashMenu/SlashMenuPlugin";
15
+ import { suggestionMenuPluginKey } from "../SuggestionMenu/SuggestionPlugin";
19
16
  import { MultipleNodeSelection } from "./MultipleNodeSelection";
20
17
 
21
18
  let dragImageElement: Element | undefined;
@@ -24,7 +21,7 @@ export type SideMenuState<
24
21
  BSchema extends BlockSchema,
25
22
  I extends InlineContentSchema,
26
23
  S extends StyleSchema
27
- > = BaseUiElementState & {
24
+ > = UiElementPosition & {
28
25
  // The block that the side menu is attached to.
29
26
  block: Block<BSchema, I, S>;
30
27
  };
@@ -255,7 +252,8 @@ export class SideMenuView<
255
252
  S extends StyleSchema
256
253
  > implements PluginView
257
254
  {
258
- private sideMenuState?: SideMenuState<BSchema, I, S>;
255
+ private state?: SideMenuState<BSchema, I, S>;
256
+ private readonly emitUpdate: (state: SideMenuState<BSchema, I, S>) => void;
259
257
 
260
258
  // When true, the drag handle with be anchored at the same level as root elements
261
259
  // When false, the drag handle with be just to the left of the element
@@ -273,10 +271,16 @@ export class SideMenuView<
273
271
  constructor(
274
272
  private readonly editor: BlockNoteEditor<BSchema, I, S>,
275
273
  private readonly pmView: EditorView,
276
- private readonly updateSideMenu: (
277
- sideMenuState: SideMenuState<BSchema, I, S>
278
- ) => void
274
+ emitUpdate: (state: SideMenuState<BSchema, I, S>) => void
279
275
  ) {
276
+ this.emitUpdate = () => {
277
+ if (!this.state) {
278
+ throw new Error("Attempting to update uninitialized side menu");
279
+ }
280
+
281
+ emitUpdate(this.state);
282
+ };
283
+
280
284
  this.horizontalPosAnchoredAtRoot = true;
281
285
  this.horizontalPosAnchor = (
282
286
  this.pmView.dom.firstChild! as HTMLElement
@@ -369,17 +373,17 @@ export class SideMenuView<
369
373
  };
370
374
 
371
375
  onKeyDown = (_event: KeyboardEvent) => {
372
- if (this.sideMenuState?.show) {
373
- this.sideMenuState.show = false;
374
- this.updateSideMenu(this.sideMenuState);
376
+ if (this.state?.show) {
377
+ this.state.show = false;
378
+ this.emitUpdate(this.state);
375
379
  }
376
380
  this.menuFrozen = false;
377
381
  };
378
382
 
379
383
  onMouseDown = (_event: MouseEvent) => {
380
- if (this.sideMenuState && !this.sideMenuState.show) {
381
- this.sideMenuState.show = true;
382
- this.updateSideMenu(this.sideMenuState);
384
+ if (this.state && !this.state.show) {
385
+ this.state.show = true;
386
+ this.emitUpdate(this.state);
383
387
  }
384
388
  this.menuFrozen = false;
385
389
  };
@@ -421,9 +425,9 @@ export class SideMenuView<
421
425
  editorWrapper.contains(event.target as HTMLElement)
422
426
  )
423
427
  ) {
424
- if (this.sideMenuState?.show) {
425
- this.sideMenuState.show = false;
426
- this.updateSideMenu(this.sideMenuState);
428
+ if (this.state?.show) {
429
+ this.state.show = false;
430
+ this.emitUpdate(this.state);
427
431
  }
428
432
 
429
433
  return;
@@ -440,9 +444,9 @@ export class SideMenuView<
440
444
 
441
445
  // Closes the menu if the mouse cursor is beyond the editor vertically.
442
446
  if (!block || !this.editor.isEditable) {
443
- if (this.sideMenuState?.show) {
444
- this.sideMenuState.show = false;
445
- this.updateSideMenu(this.sideMenuState);
447
+ if (this.state?.show) {
448
+ this.state.show = false;
449
+ this.emitUpdate(this.state);
446
450
  }
447
451
 
448
452
  return;
@@ -450,7 +454,7 @@ export class SideMenuView<
450
454
 
451
455
  // Doesn't update if the menu is already open and the mouse cursor is still hovering the same block.
452
456
  if (
453
- this.sideMenuState?.show &&
457
+ this.state?.show &&
454
458
  this.hoveredBlock?.hasAttribute("data-id") &&
455
459
  this.hoveredBlock?.getAttribute("data-id") === block.id
456
460
  ) {
@@ -470,7 +474,7 @@ export class SideMenuView<
470
474
  if (this.editor.isEditable) {
471
475
  const blockContentBoundingBox = blockContent.getBoundingClientRect();
472
476
 
473
- this.sideMenuState = {
477
+ this.state = {
474
478
  show: true,
475
479
  referencePos: new DOMRect(
476
480
  this.horizontalPosAnchoredAtRoot
@@ -485,16 +489,16 @@ export class SideMenuView<
485
489
  )!,
486
490
  };
487
491
 
488
- this.updateSideMenu(this.sideMenuState);
492
+ this.emitUpdate(this.state);
489
493
  }
490
494
  };
491
495
 
492
496
  onScroll = () => {
493
- if (this.sideMenuState?.show) {
497
+ if (this.state?.show) {
494
498
  const blockContent = this.hoveredBlock!.firstChild as HTMLElement;
495
499
  const blockContentBoundingBox = blockContent.getBoundingClientRect();
496
500
 
497
- this.sideMenuState.referencePos = new DOMRect(
501
+ this.state.referencePos = new DOMRect(
498
502
  this.horizontalPosAnchoredAtRoot
499
503
  ? this.horizontalPosAnchor
500
504
  : blockContentBoundingBox.x,
@@ -502,16 +506,16 @@ export class SideMenuView<
502
506
  blockContentBoundingBox.width,
503
507
  blockContentBoundingBox.height
504
508
  );
505
- this.updateSideMenu(this.sideMenuState);
509
+ this.emitUpdate(this.state);
506
510
  }
507
511
  };
508
512
 
509
513
  destroy() {
510
- if (this.sideMenuState?.show) {
511
- this.sideMenuState.show = false;
512
- this.updateSideMenu(this.sideMenuState);
514
+ if (this.state?.show) {
515
+ this.state.show = false;
516
+ this.emitUpdate(this.state);
513
517
  }
514
- document.body.removeEventListener("mousemove", this.onMouseMove);
518
+ document.body.removeEventListener("mousemove", this.onMouseMove, true);
515
519
  document.body.removeEventListener("dragover", this.onDragOver);
516
520
  this.pmView.dom.removeEventListener("dragstart", this.onDragStart);
517
521
  document.body.removeEventListener("drop", this.onDrop, true);
@@ -521,9 +525,9 @@ export class SideMenuView<
521
525
  }
522
526
 
523
527
  addBlock() {
524
- if (this.sideMenuState?.show) {
525
- this.sideMenuState.show = false;
526
- this.updateSideMenu(this.sideMenuState);
528
+ if (this.state?.show) {
529
+ this.state.show = false;
530
+ this.emitUpdate(this.state);
527
531
  }
528
532
 
529
533
  this.menuFrozen = true;
@@ -560,7 +564,7 @@ export class SideMenuView<
560
564
  this.editor._tiptapEditor
561
565
  .chain()
562
566
  .BNCreateBlock(newBlockInsertionPos)
563
- .BNUpdateBlock(newBlockContentPos, { type: "paragraph", props: {} })
567
+ // .BNUpdateBlock(newBlockContentPos, { type: "paragraph", props: {} })
564
568
  .setTextSelection(newBlockContentPos)
565
569
  .run();
566
570
  } else {
@@ -570,10 +574,9 @@ export class SideMenuView<
570
574
  // Focuses and activates the suggestion menu.
571
575
  this.pmView.focus();
572
576
  this.pmView.dispatch(
573
- this.pmView.state.tr.scrollIntoView().setMeta(slashMenuPluginKey, {
574
- // TODO import suggestion plugin key
575
- activate: true,
576
- type: "drag",
577
+ this.pmView.state.tr.scrollIntoView().setMeta(suggestionMenuPluginKey, {
578
+ triggerCharacter: "/",
579
+ fromUserInput: false,
577
580
  })
578
581
  );
579
582
  }
@@ -586,7 +589,7 @@ export class SideMenuProsemirrorPlugin<
586
589
  I extends InlineContentSchema,
587
590
  S extends StyleSchema
588
591
  > extends EventEmitter<any> {
589
- private sideMenuView: SideMenuView<BSchema, I, S> | undefined;
592
+ public view: SideMenuView<BSchema, I, S> | undefined;
590
593
  public readonly plugin: Plugin;
591
594
 
592
595
  constructor(private readonly editor: BlockNoteEditor<BSchema, I, S>) {
@@ -594,14 +597,10 @@ export class SideMenuProsemirrorPlugin<
594
597
  this.plugin = new Plugin({
595
598
  key: sideMenuPluginKey,
596
599
  view: (editorView) => {
597
- this.sideMenuView = new SideMenuView(
598
- editor,
599
- editorView,
600
- (sideMenuState) => {
601
- this.emit("update", sideMenuState);
602
- }
603
- );
604
- return this.sideMenuView;
600
+ this.view = new SideMenuView(editor, editorView, (state) => {
601
+ this.emit("update", state);
602
+ });
603
+ return this.view;
605
604
  },
606
605
  });
607
606
  }
@@ -614,7 +613,7 @@ export class SideMenuProsemirrorPlugin<
614
613
  * If the block is empty, opens the slash menu. If the block has content,
615
614
  * creates a new block below and opens the slash menu in it.
616
615
  */
617
- addBlock = () => this.sideMenuView!.addBlock();
616
+ addBlock = () => this.view!.addBlock();
618
617
 
619
618
  /**
620
619
  * Handles drag & drop events for blocks.
@@ -623,7 +622,7 @@ export class SideMenuProsemirrorPlugin<
623
622
  dataTransfer: DataTransfer | null;
624
623
  clientY: number;
625
624
  }) => {
626
- this.sideMenuView!.isDragging = true;
625
+ this.view!.isDragging = true;
627
626
  dragStart(event, this.editor);
628
627
  };
629
628
 
@@ -636,11 +635,11 @@ export class SideMenuProsemirrorPlugin<
636
635
  * attached to the same block regardless of which block is hovered by the
637
636
  * mouse cursor.
638
637
  */
639
- freezeMenu = () => (this.sideMenuView!.menuFrozen = true);
638
+ freezeMenu = () => (this.view!.menuFrozen = true);
640
639
  /**
641
640
  * Unfreezes the side menu. When frozen, the side menu will stay
642
641
  * attached to the same block regardless of which block is hovered by the
643
642
  * mouse cursor.
644
643
  */
645
- unfreezeMenu = () => (this.sideMenuView!.menuFrozen = false);
644
+ unfreezeMenu = () => (this.view!.menuFrozen = false);
646
645
  }
@@ -0,0 +1,8 @@
1
+ export type DefaultSuggestionItem = {
2
+ title: string;
3
+ onItemClick: () => void;
4
+ subtext?: string;
5
+ badge?: string;
6
+ aliases?: string[];
7
+ group?: string;
8
+ };