@blocknote/core 0.30.1 → 0.31.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 (139) hide show
  1. package/dist/blocknote.cjs +9 -9
  2. package/dist/blocknote.cjs.map +1 -1
  3. package/dist/blocknote.js +2754 -2230
  4. package/dist/blocknote.js.map +1 -1
  5. package/dist/{en-D4taoCs4.cjs → en-BXVKCwYt.cjs} +2 -2
  6. package/dist/en-BXVKCwYt.cjs.map +1 -0
  7. package/dist/{en-B7ycW7c8.js → en-qGo6sk9V.js} +2 -3
  8. package/dist/en-qGo6sk9V.js.map +1 -0
  9. package/dist/locales.cjs +1 -1
  10. package/dist/locales.cjs.map +1 -1
  11. package/dist/locales.js +20 -39
  12. package/dist/locales.js.map +1 -1
  13. package/dist/style.css +1 -1
  14. package/dist/webpack-stats.json +1 -1
  15. package/package.json +4 -5
  16. package/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts +2 -3
  17. package/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +1 -1
  18. package/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap +2816 -0
  19. package/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts +158 -0
  20. package/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +87 -17
  21. package/src/api/blockManipulation/selections/selection.ts +48 -1
  22. package/src/api/blockManipulation/selections/{textCursorPosition/textCursorPosition.ts → textCursorPosition.ts} +7 -7
  23. package/src/api/getBlockInfoFromPos.ts +1 -1
  24. package/src/api/nodeConversions/blockToNode.ts +5 -2
  25. package/src/api/nodeConversions/nodeToBlock.ts +203 -8
  26. package/src/api/pmUtil.ts +3 -3
  27. package/src/blocks/CodeBlockContent/CodeBlockContent.ts +6 -6
  28. package/src/blocks/FileBlockContent/helpers/render/createAddFileButton.ts +1 -1
  29. package/src/blocks/TableBlockContent/TableBlockContent.ts +32 -2
  30. package/src/editor/Block.css +27 -1
  31. package/src/editor/BlockNoteEditor.test.ts +7 -0
  32. package/src/editor/BlockNoteEditor.ts +88 -37
  33. package/src/editor/BlockNoteExtension.ts +26 -0
  34. package/src/editor/BlockNoteExtensions.ts +28 -12
  35. package/src/editor/BlockNoteTipTapEditor.ts +23 -2
  36. package/src/extensions/Collaboration/CursorPlugin.ts +13 -7
  37. package/src/extensions/Collaboration/ForkYDocPlugin.test.ts +166 -0
  38. package/src/extensions/Collaboration/ForkYDocPlugin.ts +174 -0
  39. package/src/extensions/Collaboration/SyncPlugin.ts +7 -4
  40. package/src/extensions/Collaboration/UndoPlugin.ts +7 -4
  41. package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor-forked.json +30 -0
  42. package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor.json +30 -0
  43. package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-forked.html +1 -0
  44. package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap.html +1 -0
  45. package/src/extensions/Comments/CommentsPlugin.ts +75 -70
  46. package/src/extensions/FilePanel/FilePanelPlugin.ts +50 -49
  47. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +56 -26
  48. package/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +22 -21
  49. package/src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboardPlugin.ts +45 -42
  50. package/src/extensions/Placeholder/PlaceholderPlugin.ts +111 -108
  51. package/src/extensions/PreviousBlockType/PreviousBlockTypePlugin.ts +179 -170
  52. package/src/extensions/ShowSelection/ShowSelectionPlugin.ts +22 -19
  53. package/src/extensions/SideMenu/SideMenuPlugin.ts +19 -18
  54. package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +168 -168
  55. package/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +4 -4
  56. package/src/extensions/Suggestions/SuggestionMarks.ts +175 -0
  57. package/src/extensions/TableHandles/TableHandlesPlugin.ts +153 -150
  58. package/src/i18n/locales/ar.ts +0 -1
  59. package/src/i18n/locales/de.ts +0 -1
  60. package/src/i18n/locales/en.ts +0 -1
  61. package/src/i18n/locales/es.ts +0 -1
  62. package/src/i18n/locales/fr.ts +0 -1
  63. package/src/i18n/locales/hr.ts +0 -1
  64. package/src/i18n/locales/is.ts +0 -1
  65. package/src/i18n/locales/it.ts +0 -1
  66. package/src/i18n/locales/ja.ts +0 -1
  67. package/src/i18n/locales/ko.ts +0 -1
  68. package/src/i18n/locales/nl.ts +0 -1
  69. package/src/i18n/locales/no.ts +0 -1
  70. package/src/i18n/locales/pl.ts +0 -1
  71. package/src/i18n/locales/pt.ts +0 -1
  72. package/src/i18n/locales/ru.ts +0 -1
  73. package/src/i18n/locales/sk.ts +0 -1
  74. package/src/i18n/locales/uk.ts +0 -1
  75. package/src/i18n/locales/vi.ts +0 -1
  76. package/src/i18n/locales/zh-tw.ts +0 -1
  77. package/src/i18n/locales/zh.ts +0 -1
  78. package/src/index.ts +18 -8
  79. package/src/pm-nodes/BlockContainer.ts +1 -1
  80. package/src/pm-nodes/BlockGroup.ts +1 -1
  81. package/src/pm-nodes/Doc.ts +1 -0
  82. package/types/src/api/blockManipulation/commands/insertBlocks/insertBlocks.d.ts +1 -1
  83. package/types/src/api/blockManipulation/commands/removeBlocks/removeBlocks.d.ts +4 -0
  84. package/types/src/api/blockManipulation/commands/removeBlocks/removeBlocks.test.d.ts +1 -0
  85. package/types/src/api/blockManipulation/commands/updateBlock/updateBlock.d.ts +3 -1
  86. package/types/src/api/blockManipulation/selections/selection.d.ts +10 -0
  87. package/types/src/api/blockManipulation/selections/textCursorPosition.d.ts +5 -0
  88. package/types/src/api/blockManipulation/transactions.test.d.ts +0 -0
  89. package/types/src/api/clipboard/clipboardExternal.test.d.ts +1 -0
  90. package/types/src/api/clipboard/clipboardInternal.test.d.ts +1 -0
  91. package/types/src/api/clipboard/testUtil.d.ts +541 -0
  92. package/types/src/api/exporters/html/htmlConversion.test.d.ts +1 -0
  93. package/types/src/api/exporters/markdown/markdownExporter.test.d.ts +1 -0
  94. package/types/src/api/nodeConversions/nodeConversions.test.d.ts +1 -0
  95. package/types/src/api/nodeConversions/nodeToBlock.d.ts +39 -2
  96. package/types/src/api/parsers/html/parseHTML.test.d.ts +1 -0
  97. package/types/src/api/parsers/markdown/parseMarkdown.test.d.ts +1 -0
  98. package/types/src/api/pmUtil.d.ts +3 -3
  99. package/types/src/api/testUtil/cases/customBlocks.d.ts +670 -0
  100. package/types/src/api/testUtil/cases/customInlineContent.d.ts +558 -0
  101. package/types/src/api/testUtil/cases/customStyles.d.ts +552 -0
  102. package/types/src/api/testUtil/cases/defaultSchema.d.ts +4 -0
  103. package/types/src/api/testUtil/index.d.ts +14 -0
  104. package/types/src/api/testUtil/partialBlockTestUtil.d.ts +9 -0
  105. package/types/src/api/testUtil/paste.d.ts +2 -0
  106. package/types/src/blocks/CodeBlockContent/defaultSupportedLanguages.d.ts +6 -0
  107. package/types/src/blocks/TableBlockContent/TableBlockContent.d.ts +9 -1
  108. package/types/src/editor/BlockNoteEditor.d.ts +55 -9
  109. package/types/src/editor/BlockNoteExtension.d.ts +9 -0
  110. package/types/src/editor/BlockNoteExtensions.d.ts +2 -2
  111. package/types/src/editor/BlockNoteTipTapEditor.d.ts +2 -2
  112. package/types/src/extensions/Collaboration/CursorPlugin.d.ts +3 -3
  113. package/types/src/extensions/Collaboration/ForkYDocPlugin.d.ts +41 -0
  114. package/types/src/extensions/Collaboration/ForkYDocPlugin.test.d.ts +1 -0
  115. package/types/src/extensions/Collaboration/SyncPlugin.d.ts +3 -3
  116. package/types/src/extensions/Collaboration/UndoPlugin.d.ts +3 -3
  117. package/types/src/extensions/Collaboration/createCollaborationExtensions.d.ts +17 -0
  118. package/types/src/extensions/Comments/CommentsPlugin.d.ts +2 -4
  119. package/types/src/extensions/FilePanel/FilePanelPlugin.d.ts +3 -4
  120. package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +5 -5
  121. package/types/src/extensions/LinkToolbar/LinkToolbarPlugin.d.ts +3 -4
  122. package/types/src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboardPlugin.d.ts +2 -3
  123. package/types/src/extensions/Placeholder/PlaceholderPlugin.d.ts +2 -3
  124. package/types/src/extensions/PreviousBlockType/PreviousBlockTypePlugin.d.ts +2 -3
  125. package/types/src/extensions/ShowSelection/ShowSelectionPlugin.d.ts +2 -3
  126. package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +3 -4
  127. package/types/src/extensions/SuggestionMenu/SuggestionPlugin.d.ts +2 -4
  128. package/types/src/extensions/Suggestions/SuggestionMarks.d.ts +4 -0
  129. package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +5 -6
  130. package/types/src/i18n/locales/en.d.ts +0 -1
  131. package/types/src/i18n/locales/sk.d.ts +0 -1
  132. package/types/src/index.d.ts +15 -8
  133. package/dist/en-B7ycW7c8.js.map +0 -1
  134. package/dist/en-D4taoCs4.cjs.map +0 -1
  135. package/dist/tsconfig.tsbuildinfo +0 -1
  136. package/src/api/blockManipulation/selections/__snapshots__/selection.test.ts.snap +0 -844
  137. package/src/api/blockManipulation/selections/selection.test.ts +0 -72
  138. package/src/api/blockManipulation/selections/textCursorPosition/__snapshots__/textCursorPosition.test.ts.snap +0 -316
  139. package/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts +0 -74
@@ -9,7 +9,7 @@ import type {
9
9
  User,
10
10
  } from "../../comments/index.js";
11
11
  import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
12
- import { EventEmitter } from "../../util/EventEmitter.js";
12
+ import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
13
13
  import { UserStore } from "./userstore/UserStore.js";
14
14
 
15
15
  const PLUGIN_KEY = new PluginKey(`blocknote-comments`);
@@ -56,8 +56,7 @@ function getUpdatedThreadPositions(doc: Node, markType: string) {
56
56
  return threadPositions;
57
57
  }
58
58
 
59
- export class CommentsPlugin extends EventEmitter<any> {
60
- public readonly plugin: Plugin;
59
+ export class CommentsPlugin extends BlockNoteExtension {
61
60
  public readonly userStore: UserStore<User>;
62
61
 
63
62
  /**
@@ -103,6 +102,7 @@ export class CommentsPlugin extends EventEmitter<any> {
103
102
  const trimmedTo = Math.min(
104
103
  pos + node.nodeSize,
105
104
  tr.doc.content.size - 1,
105
+ tr.doc.content.size - 1,
106
106
  );
107
107
  tr.removeMark(trimmedFrom, trimmedTo, mark);
108
108
  tr.addMark(
@@ -156,86 +156,91 @@ export class CommentsPlugin extends EventEmitter<any> {
156
156
  // eslint-disable-next-line @typescript-eslint/no-this-alias
157
157
  const self = this;
158
158
 
159
- this.plugin = new Plugin<CommentsPluginState>({
160
- key: PLUGIN_KEY,
161
- state: {
162
- init() {
163
- return {
164
- decorations: DecorationSet.empty,
165
- };
166
- },
167
- apply(tr, state) {
168
- const action = tr.getMeta(PLUGIN_KEY);
169
-
170
- if (!tr.docChanged && !action) {
171
- return state;
172
- }
159
+ this.addProsemirrorPlugin(
160
+ new Plugin<CommentsPluginState>({
161
+ key: PLUGIN_KEY,
162
+ state: {
163
+ init() {
164
+ return {
165
+ decorations: DecorationSet.empty,
166
+ };
167
+ },
168
+ apply(tr, state) {
169
+ const action = tr.getMeta(PLUGIN_KEY);
170
+
171
+ if (!tr.docChanged && !action) {
172
+ return state;
173
+ }
173
174
 
174
- // only update threadPositions if the doc changed
175
- const threadPositions = tr.docChanged
176
- ? getUpdatedThreadPositions(tr.doc, self.markType)
177
- : self.threadPositions;
175
+ // only update threadPositions if the doc changed
176
+ const threadPositions = tr.docChanged
177
+ ? getUpdatedThreadPositions(tr.doc, self.markType)
178
+ : self.threadPositions;
178
179
 
179
- if (threadPositions.size > 0 || self.threadPositions.size > 0) {
180
- // small optimization; don't emit event if threadPositions before / after were both empty
181
- self.threadPositions = threadPositions;
182
- self.emitStateUpdate();
183
- }
184
-
185
- // update decorations if doc or selected thread changed
186
- const decorations = [];
180
+ if (threadPositions.size > 0 || self.threadPositions.size > 0) {
181
+ // small optimization; don't emit event if threadPositions before / after were both empty
182
+ self.threadPositions = threadPositions;
183
+ self.emitStateUpdate();
184
+ }
187
185
 
188
- if (self.selectedThreadId) {
189
- const selectedThreadPosition = threadPositions.get(
190
- self.selectedThreadId,
191
- );
186
+ // update decorations if doc or selected thread changed
187
+ const decorations = [];
192
188
 
193
- if (selectedThreadPosition) {
194
- decorations.push(
195
- Decoration.inline(
196
- selectedThreadPosition.from,
197
- selectedThreadPosition.to,
198
- {
199
- class: "bn-thread-mark-selected",
200
- },
201
- ),
189
+ if (self.selectedThreadId) {
190
+ const selectedThreadPosition = threadPositions.get(
191
+ self.selectedThreadId,
202
192
  );
193
+
194
+ if (selectedThreadPosition) {
195
+ decorations.push(
196
+ Decoration.inline(
197
+ selectedThreadPosition.from,
198
+ selectedThreadPosition.to,
199
+ {
200
+ class: "bn-thread-mark-selected",
201
+ },
202
+ ),
203
+ );
204
+ }
203
205
  }
204
- }
205
206
 
206
- return {
207
- decorations: DecorationSet.create(tr.doc, decorations),
208
- };
209
- },
210
- },
211
- props: {
212
- decorations(state) {
213
- return PLUGIN_KEY.getState(state)?.decorations ?? DecorationSet.empty;
207
+ return {
208
+ decorations: DecorationSet.create(tr.doc, decorations),
209
+ };
210
+ },
214
211
  },
215
- /**
216
- * Handle click on a thread mark and mark it as selected
217
- */
218
- handleClick: (view, pos, event) => {
219
- if (event.button !== 0) {
220
- return;
221
- }
212
+ props: {
213
+ decorations(state) {
214
+ return (
215
+ PLUGIN_KEY.getState(state)?.decorations ?? DecorationSet.empty
216
+ );
217
+ },
218
+ /**
219
+ * Handle click on a thread mark and mark it as selected
220
+ */
221
+ handleClick: (view, pos, event) => {
222
+ if (event.button !== 0) {
223
+ return;
224
+ }
222
225
 
223
- const node = view.state.doc.nodeAt(pos);
226
+ const node = view.state.doc.nodeAt(pos);
224
227
 
225
- if (!node) {
226
- self.selectThread(undefined);
227
- return;
228
- }
228
+ if (!node) {
229
+ self.selectThread(undefined);
230
+ return;
231
+ }
229
232
 
230
- const commentMark = node.marks.find(
231
- (mark) => mark.type.name === markType && mark.attrs.orphan !== true,
232
- );
233
+ const commentMark = node.marks.find(
234
+ (mark) =>
235
+ mark.type.name === markType && mark.attrs.orphan !== true,
236
+ );
233
237
 
234
- const threadId = commentMark?.attrs.threadId as string | undefined;
235
- self.selectThread(threadId, false);
238
+ const threadId = commentMark?.attrs.threadId as string | undefined;
239
+ self.selectThread(threadId, false);
240
+ },
236
241
  },
237
- },
238
- });
242
+ }),
243
+ );
239
244
  }
240
245
 
241
246
  /**
@@ -1,7 +1,9 @@
1
1
  import { EditorState, Plugin, PluginKey, PluginView } from "prosemirror-state";
2
2
  import { EditorView } from "prosemirror-view";
3
3
 
4
+ import { ySyncPluginKey } from "y-prosemirror";
4
5
  import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
6
+ import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
5
7
  import { UiElementPosition } from "../../extensions-shared/UiElementPosition.js";
6
8
  import type {
7
9
  BlockFromConfig,
@@ -9,8 +11,6 @@ import type {
9
11
  InlineContentSchema,
10
12
  StyleSchema,
11
13
  } from "../../schema/index.js";
12
- import { EventEmitter } from "../../util/EventEmitter.js";
13
- import { ySyncPluginKey } from "y-prosemirror";
14
14
 
15
15
  export type FilePanelState<
16
16
  I extends InlineContentSchema,
@@ -138,60 +138,61 @@ const filePanelPluginKey = new PluginKey<FilePanelState<any, any>>(
138
138
  export class FilePanelProsemirrorPlugin<
139
139
  I extends InlineContentSchema,
140
140
  S extends StyleSchema,
141
- > extends EventEmitter<any> {
141
+ > extends BlockNoteExtension {
142
142
  private view: FilePanelView<I, S> | undefined;
143
- public readonly plugin: Plugin;
144
143
 
145
144
  constructor(editor: BlockNoteEditor<Record<string, FileBlockConfig>, I, S>) {
146
145
  super();
147
- this.plugin = new Plugin<{
148
- block: BlockFromConfig<FileBlockConfig, I, S> | undefined;
149
- }>({
150
- key: filePanelPluginKey,
151
- view: (editorView) => {
152
- this.view = new FilePanelView<I, S>(
153
- editor,
154
- filePanelPluginKey,
155
- editorView,
156
- (state) => {
157
- this.emit("update", state);
158
- },
159
- );
160
- return this.view;
161
- },
162
- props: {
163
- handleKeyDown: (_view, event: KeyboardEvent) => {
164
- if (event.key === "Escape" && this.shown) {
165
- this.view?.closeMenu();
166
- return true;
167
- }
168
- return false;
146
+ this.addProsemirrorPlugin(
147
+ new Plugin<{
148
+ block: BlockFromConfig<FileBlockConfig, I, S> | undefined;
149
+ }>({
150
+ key: filePanelPluginKey,
151
+ view: (editorView) => {
152
+ this.view = new FilePanelView<I, S>(
153
+ editor,
154
+ filePanelPluginKey,
155
+ editorView,
156
+ (state) => {
157
+ this.emit("update", state);
158
+ },
159
+ );
160
+ return this.view;
169
161
  },
170
- },
171
- state: {
172
- init: () => {
173
- return {
174
- block: undefined,
175
- };
162
+ props: {
163
+ handleKeyDown: (_view, event: KeyboardEvent) => {
164
+ if (event.key === "Escape" && this.shown) {
165
+ this.view?.closeMenu();
166
+ return true;
167
+ }
168
+ return false;
169
+ },
176
170
  },
177
- apply: (transaction, prev) => {
178
- const state: FilePanelState<I, S> | undefined =
179
- transaction.getMeta(filePanelPluginKey);
180
-
181
- if (state) {
182
- return state;
183
- }
184
-
185
- if (
186
- !transaction.getMeta(ySyncPluginKey) &&
187
- (transaction.selectionSet || transaction.docChanged)
188
- ) {
189
- return { block: undefined };
190
- }
191
- return prev;
171
+ state: {
172
+ init: () => {
173
+ return {
174
+ block: undefined,
175
+ };
176
+ },
177
+ apply: (transaction, prev) => {
178
+ const state: FilePanelState<I, S> | undefined =
179
+ transaction.getMeta(filePanelPluginKey);
180
+
181
+ if (state) {
182
+ return state;
183
+ }
184
+
185
+ if (
186
+ !transaction.getMeta(ySyncPluginKey) &&
187
+ (transaction.selectionSet || transaction.docChanged)
188
+ ) {
189
+ return { block: undefined };
190
+ }
191
+ return prev;
192
+ },
192
193
  },
193
- },
194
- });
194
+ }),
195
+ );
195
196
  }
196
197
 
197
198
  public get shown() {
@@ -3,13 +3,13 @@ import { EditorState, Plugin, PluginKey, PluginView } from "prosemirror-state";
3
3
  import { EditorView } from "prosemirror-view";
4
4
 
5
5
  import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
6
+ import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
6
7
  import { UiElementPosition } from "../../extensions-shared/UiElementPosition.js";
7
8
  import {
8
9
  BlockSchema,
9
10
  InlineContentSchema,
10
11
  StyleSchema,
11
12
  } from "../../schema/index.js";
12
- import { EventEmitter } from "../../util/EventEmitter.js";
13
13
 
14
14
  export type FormattingToolbarState = UiElementPosition;
15
15
 
@@ -25,7 +25,7 @@ export class FormattingToolbarView implements PluginView {
25
25
  state: EditorState;
26
26
  from: number;
27
27
  to: number;
28
- }) => boolean = ({ state, from, to }) => {
28
+ }) => boolean = ({ view, state, from, to }) => {
29
29
  const { doc, selection } = state;
30
30
  const { empty } = selection;
31
31
 
@@ -43,7 +43,16 @@ export class FormattingToolbarView implements PluginView {
43
43
  return false;
44
44
  }
45
45
 
46
- return !(empty || isEmptyTextBlock);
46
+ if (empty || isEmptyTextBlock) {
47
+ return false;
48
+ }
49
+
50
+ const focusedElement = document.activeElement;
51
+ if (!this.isElementWithinEditorWrapper(focusedElement) && view.editable) {
52
+ // editable editors must have focus for the toolbar to show
53
+ return false;
54
+ }
55
+ return true;
47
56
  };
48
57
 
49
58
  constructor(
@@ -108,8 +117,22 @@ export class FormattingToolbarView implements PluginView {
108
117
  }
109
118
  };
110
119
 
111
- viewMousedownHandler = () => {
112
- this.preventShow = true;
120
+ isElementWithinEditorWrapper = (element: Node | null) => {
121
+ if (!element) {
122
+ return false;
123
+ }
124
+ const editorWrapper = this.pmView.dom.parentElement;
125
+ if (!editorWrapper) {
126
+ return false;
127
+ }
128
+
129
+ return editorWrapper.contains(element);
130
+ };
131
+
132
+ viewMousedownHandler = (e: MouseEvent) => {
133
+ if (!this.isElementWithinEditorWrapper(e.target as Node)) {
134
+ this.preventShow = true;
135
+ }
113
136
  };
114
137
 
115
138
  mouseupHandler = () => {
@@ -172,12 +195,18 @@ export class FormattingToolbarView implements PluginView {
172
195
  // e.g. the download file button, should still be accessible. Therefore,
173
196
  // logic for hiding when the editor is non-editable is handled
174
197
  // individually in each button.
175
- this.state = {
198
+ const nextState = {
176
199
  show: true,
177
200
  referencePos: this.getSelectionBoundingBox(),
178
201
  };
179
202
 
180
- this.emitUpdate();
203
+ if (
204
+ nextState.show !== this.state?.show ||
205
+ nextState.referencePos.toJSON() !== this.state?.referencePos.toJSON()
206
+ ) {
207
+ this.state = nextState;
208
+ this.emitUpdate();
209
+ }
181
210
 
182
211
  return;
183
212
  }
@@ -236,30 +265,31 @@ export const formattingToolbarPluginKey = new PluginKey(
236
265
  "FormattingToolbarPlugin",
237
266
  );
238
267
 
239
- export class FormattingToolbarProsemirrorPlugin extends EventEmitter<any> {
268
+ export class FormattingToolbarProsemirrorPlugin extends BlockNoteExtension {
240
269
  private view: FormattingToolbarView | undefined;
241
- public readonly plugin: Plugin;
242
270
 
243
271
  constructor(editor: BlockNoteEditor<any, any, any>) {
244
272
  super();
245
- this.plugin = new Plugin({
246
- key: formattingToolbarPluginKey,
247
- view: (editorView) => {
248
- this.view = new FormattingToolbarView(editor, editorView, (state) => {
249
- this.emit("update", state);
250
- });
251
- return this.view;
252
- },
253
- props: {
254
- handleKeyDown: (_view, event: KeyboardEvent) => {
255
- if (event.key === "Escape" && this.shown) {
256
- this.view!.closeMenu();
257
- return true;
258
- }
259
- return false;
273
+ this.addProsemirrorPlugin(
274
+ new Plugin({
275
+ key: formattingToolbarPluginKey,
276
+ view: (editorView) => {
277
+ this.view = new FormattingToolbarView(editor, editorView, (state) => {
278
+ this.emit("update", state);
279
+ });
280
+ return this.view;
260
281
  },
261
- },
262
- });
282
+ props: {
283
+ handleKeyDown: (_view, event: KeyboardEvent) => {
284
+ if (event.key === "Escape" && this.shown) {
285
+ this.view!.closeMenu();
286
+ return true;
287
+ }
288
+ return false;
289
+ },
290
+ },
291
+ }),
292
+ );
263
293
  }
264
294
 
265
295
  public get shown() {
@@ -4,15 +4,15 @@ import { EditorView } from "@tiptap/pm/view";
4
4
  import { Mark } from "prosemirror-model";
5
5
  import { EditorState, Plugin, PluginKey, PluginView } from "prosemirror-state";
6
6
 
7
+ import { getPmSchema } from "../../api/pmUtil.js";
7
8
  import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
9
+ import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
8
10
  import { UiElementPosition } from "../../extensions-shared/UiElementPosition.js";
9
11
  import {
10
12
  BlockSchema,
11
13
  InlineContentSchema,
12
14
  StyleSchema,
13
15
  } from "../../schema/index.js";
14
- import { EventEmitter } from "../../util/EventEmitter.js";
15
- import { getPmSchema } from "../../api/pmUtil.js";
16
16
 
17
17
  export type LinkToolbarState = UiElementPosition & {
18
18
  // The hovered link's URL, and the text it's displayed with in the
@@ -301,30 +301,31 @@ export class LinkToolbarProsemirrorPlugin<
301
301
  BSchema extends BlockSchema,
302
302
  I extends InlineContentSchema,
303
303
  S extends StyleSchema,
304
- > extends EventEmitter<any> {
304
+ > extends BlockNoteExtension {
305
305
  private view: LinkToolbarView | undefined;
306
- public readonly plugin: Plugin;
307
306
 
308
307
  constructor(editor: BlockNoteEditor<BSchema, I, S>) {
309
308
  super();
310
- this.plugin = new Plugin({
311
- key: linkToolbarPluginKey,
312
- view: (editorView) => {
313
- this.view = new LinkToolbarView(editor, editorView, (state) => {
314
- this.emit("update", state);
315
- });
316
- return this.view;
317
- },
318
- props: {
319
- handleKeyDown: (_view, event: KeyboardEvent) => {
320
- if (event.key === "Escape" && this.shown) {
321
- this.view!.closeMenu();
322
- return true;
323
- }
324
- return false;
309
+ this.addProsemirrorPlugin(
310
+ new Plugin({
311
+ key: linkToolbarPluginKey,
312
+ view: (editorView) => {
313
+ this.view = new LinkToolbarView(editor, editorView, (state) => {
314
+ this.emit("update", state);
315
+ });
316
+ return this.view;
325
317
  },
326
- },
327
- });
318
+ props: {
319
+ handleKeyDown: (_view, event: KeyboardEvent) => {
320
+ if (event.key === "Escape" && this.shown) {
321
+ this.view!.closeMenu();
322
+ return true;
323
+ }
324
+ return false;
325
+ },
326
+ },
327
+ }),
328
+ );
328
329
  }
329
330
 
330
331
  public onUpdate(callback: (state: LinkToolbarState) => void) {
@@ -1,4 +1,5 @@
1
1
  import { Plugin, PluginKey, TextSelection } from "prosemirror-state";
2
+ import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
2
3
 
3
4
  const PLUGIN_KEY = new PluginKey("node-selection-keyboard");
4
5
  // By default, typing with a node selection active will cause ProseMirror to
@@ -15,54 +16,56 @@ const PLUGIN_KEY = new PluginKey("node-selection-keyboard");
15
16
  // While a more elegant solution would probably process transactions instead of
16
17
  // keystrokes, this brings us most of the way to Notion's UX without much added
17
18
  // complexity.
18
- export class NodeSelectionKeyboardPlugin {
19
- public readonly plugin: Plugin;
19
+ export class NodeSelectionKeyboardPlugin extends BlockNoteExtension {
20
20
  constructor() {
21
- this.plugin = new Plugin({
22
- key: PLUGIN_KEY,
23
- props: {
24
- handleKeyDown: (view, event) => {
25
- // Checks for node selection
26
- if ("node" in view.state.selection) {
27
- // Checks if key press uses ctrl/meta modifier
28
- if (event.ctrlKey || event.metaKey) {
29
- return false;
30
- }
31
- // Checks if key press is alphanumeric
32
- if (event.key.length === 1) {
33
- event.preventDefault();
21
+ super();
22
+ this.addProsemirrorPlugin(
23
+ new Plugin({
24
+ key: PLUGIN_KEY,
25
+ props: {
26
+ handleKeyDown: (view, event) => {
27
+ // Checks for node selection
28
+ if ("node" in view.state.selection) {
29
+ // Checks if key press uses ctrl/meta modifier
30
+ if (event.ctrlKey || event.metaKey) {
31
+ return false;
32
+ }
33
+ // Checks if key press is alphanumeric
34
+ if (event.key.length === 1) {
35
+ event.preventDefault();
34
36
 
35
- return true;
36
- }
37
- // Checks if key press is Enter
38
- if (
39
- event.key === "Enter" &&
40
- !event.shiftKey &&
41
- !event.altKey &&
42
- !event.ctrlKey &&
43
- !event.metaKey
44
- ) {
45
- const tr = view.state.tr;
46
- view.dispatch(
47
- tr
48
- .insert(
49
- view.state.tr.selection.$to.after(),
50
- view.state.schema.nodes["paragraph"].createChecked(),
51
- )
52
- .setSelection(
53
- new TextSelection(
54
- tr.doc.resolve(view.state.tr.selection.$to.after() + 1),
37
+ return true;
38
+ }
39
+ // Checks if key press is Enter
40
+ if (
41
+ event.key === "Enter" &&
42
+ !event.shiftKey &&
43
+ !event.altKey &&
44
+ !event.ctrlKey &&
45
+ !event.metaKey
46
+ ) {
47
+ const tr = view.state.tr;
48
+ view.dispatch(
49
+ tr
50
+ .insert(
51
+ view.state.tr.selection.$to.after(),
52
+ view.state.schema.nodes["paragraph"].createChecked(),
53
+ )
54
+ .setSelection(
55
+ new TextSelection(
56
+ tr.doc.resolve(view.state.tr.selection.$to.after() + 1),
57
+ ),
55
58
  ),
56
- ),
57
- );
59
+ );
58
60
 
59
- return true;
61
+ return true;
62
+ }
60
63
  }
61
- }
62
64
 
63
- return false;
65
+ return false;
66
+ },
64
67
  },
65
- },
66
- });
68
+ }),
69
+ );
67
70
  }
68
71
  }