@blocknote/core 0.8.0 → 0.8.2

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 (34) hide show
  1. package/dist/blocknote.js +594 -503
  2. package/dist/blocknote.js.map +1 -1
  3. package/dist/blocknote.umd.cjs +6 -2
  4. package/dist/blocknote.umd.cjs.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +3 -3
  7. package/src/BlockNoteEditor.ts +18 -0
  8. package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +292 -0
  9. package/src/api/nodeConversions/nodeConversions.test.ts +236 -0
  10. package/src/api/nodeConversions/nodeConversions.ts +166 -35
  11. package/src/editor.module.css +5 -5
  12. package/src/extensions/Blocks/nodes/Block.module.css +4 -4
  13. package/src/extensions/Blocks/nodes/BlockContainer.ts +12 -3
  14. package/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts +2 -2
  15. package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +59 -24
  16. package/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +4 -6
  17. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +28 -35
  18. package/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts +2 -2
  19. package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +38 -10
  20. package/src/shared/EditorElement.ts +3 -3
  21. package/src/shared/plugins/suggestion/SuggestionPlugin.ts +23 -17
  22. package/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts +2 -2
  23. package/types/src/BlockNoteEditor.d.ts +2 -0
  24. package/types/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.d.ts +1 -1
  25. package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +2 -2
  26. package/types/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.d.ts +2 -3
  27. package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +2 -3
  28. package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.d.ts +1 -1
  29. package/types/src/extensions/Placeholder/localisation/index.d.ts +2 -0
  30. package/types/src/extensions/Placeholder/localisation/translation.d.ts +51 -0
  31. package/types/src/shared/EditorElement.d.ts +3 -3
  32. package/types/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.d.ts +1 -1
  33. package/types/src/extensions/Blocks/api/alertBlock.d.ts +0 -13
  34. package/types/src/extensions/Blocks/api/alertBlock2.d.ts +0 -13
@@ -10,7 +10,6 @@ import { defaultProps } from "../../extensions/Blocks/api/defaultBlocks";
10
10
  import {
11
11
  ColorStyle,
12
12
  InlineContent,
13
- Link,
14
13
  PartialInlineContent,
15
14
  PartialLink,
16
15
  StyledText,
@@ -34,7 +33,7 @@ const colorStyles = new Set<ColorStyle>(["textColor", "backgroundColor"]);
34
33
  * Convert a StyledText inline element to a
35
34
  * prosemirror text node with the appropriate marks
36
35
  */
37
- function styledTextToNode(styledText: StyledText, schema: Schema): Node {
36
+ function styledTextToNodes(styledText: StyledText, schema: Schema): Node[] {
38
37
  const marks: Mark[] = [];
39
38
 
40
39
  for (const [style, value] of Object.entries(styledText.styles)) {
@@ -45,7 +44,22 @@ function styledTextToNode(styledText: StyledText, schema: Schema): Node {
45
44
  }
46
45
  }
47
46
 
48
- return schema.text(styledText.text, marks);
47
+ return (
48
+ styledText.text
49
+ // Splits text & line breaks.
50
+ .split(/(\n)/g)
51
+ // If the content ends with a line break, an empty string is added to the
52
+ // end, which this removes.
53
+ .filter((text) => text.length > 0)
54
+ // Converts text & line breaks to nodes.
55
+ .map((text) => {
56
+ if (text === "\n") {
57
+ return schema.nodes["hardBreak"].create();
58
+ } else {
59
+ return schema.text(text, marks);
60
+ }
61
+ })
62
+ );
49
63
  }
50
64
 
51
65
  /**
@@ -58,7 +72,14 @@ function linkToNodes(link: PartialLink, schema: Schema): Node[] {
58
72
  });
59
73
 
60
74
  return styledTextArrayToNodes(link.content, schema).map((node) => {
61
- return node.mark([...node.marks, linkMark]);
75
+ if (node.type.name === "text") {
76
+ return node.mark([...node.marks, linkMark]);
77
+ }
78
+
79
+ if (node.type.name === "hardBreak") {
80
+ return node;
81
+ }
82
+ throw new Error("unexpected node type");
62
83
  });
63
84
  }
64
85
 
@@ -73,12 +94,14 @@ function styledTextArrayToNodes(
73
94
  let nodes: Node[] = [];
74
95
 
75
96
  if (typeof content === "string") {
76
- nodes.push(schema.text(content));
97
+ nodes.push(
98
+ ...styledTextToNodes({ type: "text", text: content, styles: {} }, schema)
99
+ );
77
100
  return nodes;
78
101
  }
79
102
 
80
103
  for (const styledText of content) {
81
- nodes.push(styledTextToNode(styledText, schema));
104
+ nodes.push(...styledTextToNodes(styledText, schema));
82
105
  }
83
106
  return nodes;
84
107
  }
@@ -161,15 +184,39 @@ export function blockToNode<BSchema extends BlockSchema>(
161
184
  */
162
185
  function contentNodeToInlineContent(contentNode: Node) {
163
186
  const content: InlineContent[] = [];
164
-
165
- let currentLink: Link | undefined = undefined;
187
+ let currentContent: InlineContent | undefined = undefined;
166
188
 
167
189
  // Most of the logic below is for handling links because in ProseMirror links are marks
168
190
  // while in BlockNote links are a type of inline content
169
191
  contentNode.content.forEach((node) => {
170
- const styles: Styles = {};
192
+ // hardBreak nodes do not have an InlineContent equivalent, instead we
193
+ // add a newline to the previous node.
194
+ if (node.type.name === "hardBreak") {
195
+ if (currentContent) {
196
+ // Current content exists.
197
+ if (currentContent.type === "text") {
198
+ // Current content is text.
199
+ currentContent.text += "\n";
200
+ } else if (currentContent.type === "link") {
201
+ // Current content is a link.
202
+ currentContent.content[currentContent.content.length - 1].text +=
203
+ "\n";
204
+ }
205
+ } else {
206
+ // Current content does not exist.
207
+ currentContent = {
208
+ type: "text",
209
+ text: "\n",
210
+ styles: {},
211
+ };
212
+ }
213
+
214
+ return;
215
+ }
171
216
 
217
+ const styles: Styles = {};
172
218
  let linkMark: Mark | undefined;
219
+
173
220
  for (const mark of node.marks) {
174
221
  if (mark.type.name === "link") {
175
222
  linkMark = mark;
@@ -182,37 +229,121 @@ function contentNodeToInlineContent(contentNode: Node) {
182
229
  }
183
230
  }
184
231
 
185
- if (linkMark && currentLink && linkMark.attrs.href === currentLink.href) {
186
- // if the node is a link that matches the current link, add it to the current link
187
- currentLink.content.push({
188
- type: "text",
189
- text: node.textContent,
190
- styles,
191
- });
192
- } else if (linkMark) {
193
- // if the node is a link that doesn't match the current link, create a new link
194
- currentLink = {
195
- type: "link",
196
- href: linkMark.attrs.href,
197
- content: [
198
- {
232
+ // Parsing links and text.
233
+ // Current content exists.
234
+ if (currentContent) {
235
+ // Current content is text.
236
+ if (currentContent.type === "text") {
237
+ if (!linkMark) {
238
+ // Node is text (same type as current content).
239
+ if (
240
+ JSON.stringify(currentContent.styles) === JSON.stringify(styles)
241
+ ) {
242
+ // Styles are the same.
243
+ currentContent.text += node.textContent;
244
+ } else {
245
+ // Styles are different.
246
+ content.push(currentContent);
247
+ currentContent = {
248
+ type: "text",
249
+ text: node.textContent,
250
+ styles,
251
+ };
252
+ }
253
+ } else {
254
+ // Node is a link (different type to current content).
255
+ content.push(currentContent);
256
+ currentContent = {
257
+ type: "link",
258
+ href: linkMark.attrs.href,
259
+ content: [
260
+ {
261
+ type: "text",
262
+ text: node.textContent,
263
+ styles,
264
+ },
265
+ ],
266
+ };
267
+ }
268
+ } else if (currentContent.type === "link") {
269
+ // Current content is a link.
270
+ if (linkMark) {
271
+ // Node is a link (same type as current content).
272
+ // Link URLs are the same.
273
+ if (currentContent.href === linkMark.attrs.href) {
274
+ // Styles are the same.
275
+ if (
276
+ JSON.stringify(
277
+ currentContent.content[currentContent.content.length - 1].styles
278
+ ) === JSON.stringify(styles)
279
+ ) {
280
+ currentContent.content[currentContent.content.length - 1].text +=
281
+ node.textContent;
282
+ } else {
283
+ // Styles are different.
284
+ currentContent.content.push({
285
+ type: "text",
286
+ text: node.textContent,
287
+ styles,
288
+ });
289
+ }
290
+ } else {
291
+ // Link URLs are different.
292
+ content.push(currentContent);
293
+ currentContent = {
294
+ type: "link",
295
+ href: linkMark.attrs.href,
296
+ content: [
297
+ {
298
+ type: "text",
299
+ text: node.textContent,
300
+ styles,
301
+ },
302
+ ],
303
+ };
304
+ }
305
+ } else {
306
+ // Node is text (different type to current content).
307
+ content.push(currentContent);
308
+ currentContent = {
199
309
  type: "text",
200
310
  text: node.textContent,
201
311
  styles,
202
- },
203
- ],
204
- };
205
- content.push(currentLink);
206
- } else {
207
- // if the node is not a link, add it to the content
208
- content.push({
209
- type: "text",
210
- text: node.textContent,
211
- styles,
212
- });
213
- currentLink = undefined;
312
+ };
313
+ }
314
+ }
315
+ }
316
+ // Current content does not exist.
317
+ else {
318
+ // Node is text.
319
+ if (!linkMark) {
320
+ currentContent = {
321
+ type: "text",
322
+ text: node.textContent,
323
+ styles,
324
+ };
325
+ }
326
+ // Node is a link.
327
+ else {
328
+ currentContent = {
329
+ type: "link",
330
+ href: linkMark.attrs.href,
331
+ content: [
332
+ {
333
+ type: "text",
334
+ text: node.textContent,
335
+ styles,
336
+ },
337
+ ],
338
+ };
339
+ }
214
340
  }
215
341
  });
342
+
343
+ if (currentContent) {
344
+ content.push(currentContent);
345
+ }
346
+
216
347
  return content;
217
348
  }
218
349
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  .bnEditor {
4
4
  outline: none;
5
- padding-inline: 50px;
5
+ padding-inline: 54px;
6
6
  border-radius: 8px;
7
7
 
8
8
  /* Define a set of colors to be used throughout the app for consistency
@@ -56,13 +56,13 @@ Tippy popups that are appended to document.body directly
56
56
  }
57
57
 
58
58
  [data-theme="light"] {
59
- background-color: #ffffff;
60
- color: #444444;
59
+ background-color: #FFFFFF;
60
+ color: #3F3F3F;
61
61
  }
62
62
 
63
63
  [data-theme="dark"] {
64
- background-color: #444444;
65
- color: #dddddd;
64
+ background-color: #1F1F1F;
65
+ color: #CFCFCF;
66
66
  }
67
67
 
68
68
  .dragPreview {
@@ -60,14 +60,14 @@ NESTED BLOCKS
60
60
  .blockGroup
61
61
  .blockGroup
62
62
  > .blockOuter:not([data-prev-depth-changed])::before {
63
- border-left: 1px solid #cccccc;
63
+ border-left: 1px solid #AFAFAF;
64
64
  }
65
65
 
66
66
  [data-theme="dark"]
67
67
  .blockGroup
68
68
  .blockGroup
69
69
  > .blockOuter:not([data-prev-depth-changed])::before {
70
- border-left: 1px solid #999999;
70
+ border-left: 1px solid #7F7F7F;
71
71
  }
72
72
 
73
73
  .blockGroup .blockGroup > .blockOuter[data-prev-depth-change="-2"]::before {
@@ -250,12 +250,12 @@ NESTED BLOCKS
250
250
 
251
251
  [data-theme="light"] .isEmpty .inlineContent:before,
252
252
  .isFilter .inlineContent:before {
253
- color: #cccccc;
253
+ color: #CFCFCF;
254
254
  }
255
255
 
256
256
  [data-theme="dark"] .isEmpty .inlineContent:before,
257
257
  .isFilter .inlineContent:before {
258
- color: #999999;
258
+ color: #7F7F7F;
259
259
  }
260
260
 
261
261
  /* TODO: would be nicer if defined from code */
@@ -281,10 +281,19 @@ export const BlockContainer = Node.create<IBlock>({
281
281
  }
282
282
 
283
283
  // Deletes next block and adds its text content to the nearest previous block.
284
- // TODO: Use slices.
284
+
285
285
  if (dispatch) {
286
- state.tr.deleteRange(startPos, startPos + contentNode.nodeSize);
287
- state.tr.insertText(contentNode.textContent, prevBlockEndPos - 1);
286
+ dispatch(
287
+ state.tr
288
+ .deleteRange(startPos, startPos + contentNode.nodeSize)
289
+ .replace(
290
+ prevBlockEndPos - 1,
291
+ startPos,
292
+ new Slice(contentNode.content, 0, 0)
293
+ )
294
+ .scrollIntoView()
295
+ );
296
+
288
297
  state.tr.setSelection(
289
298
  new TextSelection(state.doc.resolve(prevBlockEndPos - 1))
290
299
  );
@@ -12,12 +12,12 @@ export type BlockSideMenuStaticParams<BSchema extends BlockSchema> = {
12
12
 
13
13
  freezeMenu: () => void;
14
14
  unfreezeMenu: () => void;
15
+
16
+ getReferenceRect: () => DOMRect;
15
17
  };
16
18
 
17
19
  export type BlockSideMenuDynamicParams<BSchema extends BlockSchema> = {
18
20
  block: Block<BSchema>;
19
-
20
- referenceRect: DOMRect;
21
21
  };
22
22
 
23
23
  export type BlockSideMenu<BSchema extends BlockSchema> = EditorElement<
@@ -250,6 +250,8 @@ export class BlockMenuView<BSchema extends BlockSchema> {
250
250
  menuOpen = false;
251
251
  menuFrozen = false;
252
252
 
253
+ private lastPosition: DOMRect | undefined;
254
+
253
255
  constructor({
254
256
  tiptapEditor,
255
257
  editor,
@@ -272,9 +274,6 @@ export class BlockMenuView<BSchema extends BlockSchema> {
272
274
  // Shows or updates menu position whenever the cursor moves, if the menu isn't frozen.
273
275
  document.body.addEventListener("mousemove", this.onMouseMove, true);
274
276
 
275
- // Makes menu scroll with the page.
276
- document.addEventListener("scroll", this.onScroll);
277
-
278
277
  // Hides and unfreezes the menu whenever the user selects the editor with the mouse or presses a key.
279
278
  // TODO: Better integration with suggestions menu and only editor scope?
280
279
  document.body.addEventListener("mousedown", this.onMouseDown, true);
@@ -320,7 +319,7 @@ export class BlockMenuView<BSchema extends BlockSchema> {
320
319
  };
321
320
 
322
321
  /**
323
- * If the event is outside of the editor contents,
322
+ * If the event is outside the editor contents,
324
323
  * we dispatch a fake event, so that we can still drop the content
325
324
  * when dragging / dropping to the side of the editor
326
325
  */
@@ -375,11 +374,45 @@ export class BlockMenuView<BSchema extends BlockSchema> {
375
374
  return;
376
375
  }
377
376
 
378
- // Editor itself may have padding or other styling which affects size/position, so we get the boundingRect of
379
- // the first child (i.e. the blockGroup that wraps all blocks in the editor) for a more accurate bounding box.
377
+ // Editor itself may have padding or other styling which affects
378
+ // size/position, so we get the boundingRect of the first child (i.e. the
379
+ // blockGroup that wraps all blocks in the editor) for more accurate side
380
+ // menu placement.
380
381
  const editorBoundingBox = (
381
382
  this.ttEditor.view.dom.firstChild! as HTMLElement
382
383
  ).getBoundingClientRect();
384
+ // We want the full area of the editor to check if the cursor is hovering
385
+ // above it though.
386
+ const editorOuterBoundingBox =
387
+ this.ttEditor.view.dom.getBoundingClientRect();
388
+ const cursorWithinEditor =
389
+ event.clientX >= editorOuterBoundingBox.left &&
390
+ event.clientX <= editorOuterBoundingBox.right &&
391
+ event.clientY >= editorOuterBoundingBox.top &&
392
+ event.clientY <= editorOuterBoundingBox.bottom;
393
+
394
+ // Doesn't update if the mouse hovers an element that's over the editor but
395
+ // isn't a part of it or the side menu.
396
+ if (
397
+ // Cursor is within the editor area
398
+ cursorWithinEditor &&
399
+ // An element is hovered
400
+ event &&
401
+ event.target &&
402
+ // Element is outside the editor
403
+ this.ttEditor.view.dom !== event.target &&
404
+ !this.ttEditor.view.dom.contains(event.target as HTMLElement) &&
405
+ // Element is outside the side menu
406
+ this.blockMenu.element !== event.target &&
407
+ !this.blockMenu.element?.contains(event.target as HTMLElement)
408
+ ) {
409
+ if (this.menuOpen) {
410
+ this.menuOpen = false;
411
+ this.blockMenu.hide();
412
+ }
413
+
414
+ return;
415
+ }
383
416
 
384
417
  this.horizontalPosAnchor = editorBoundingBox.x;
385
418
 
@@ -429,12 +462,6 @@ export class BlockMenuView<BSchema extends BlockSchema> {
429
462
  }
430
463
  };
431
464
 
432
- onScroll = () => {
433
- if (this.menuOpen) {
434
- this.blockMenu.render(this.getDynamicParams(), false);
435
- }
436
- };
437
-
438
465
  destroy() {
439
466
  if (this.menuOpen) {
440
467
  this.menuOpen = false;
@@ -445,7 +472,6 @@ export class BlockMenuView<BSchema extends BlockSchema> {
445
472
  this.ttEditor.view.dom.removeEventListener("dragstart", this.onDragStart);
446
473
  document.body.removeEventListener("drop", this.onDrop);
447
474
  document.body.removeEventListener("mousedown", this.onMouseDown);
448
- document.removeEventListener("scroll", this.onScroll);
449
475
  document.body.removeEventListener("keydown", this.onKeyDown);
450
476
  }
451
477
 
@@ -514,23 +540,32 @@ export class BlockMenuView<BSchema extends BlockSchema> {
514
540
  unfreezeMenu: () => {
515
541
  this.menuFrozen = false;
516
542
  },
543
+ getReferenceRect: () => {
544
+ if (!this.menuOpen) {
545
+ if (this.lastPosition === undefined) {
546
+ throw new Error(
547
+ "Attempted to access block reference rect before rendering block side menu."
548
+ );
549
+ }
550
+
551
+ return this.lastPosition;
552
+ }
553
+
554
+ const blockContent = this.hoveredBlock!.firstChild! as HTMLElement;
555
+ const blockContentBoundingBox = blockContent.getBoundingClientRect();
556
+ if (this.horizontalPosAnchoredAtRoot) {
557
+ blockContentBoundingBox.x = this.horizontalPosAnchor;
558
+ }
559
+ this.lastPosition = blockContentBoundingBox;
560
+
561
+ return blockContentBoundingBox;
562
+ },
517
563
  };
518
564
  }
519
565
 
520
566
  getDynamicParams(): BlockSideMenuDynamicParams<BSchema> {
521
- const blockContent = this.hoveredBlock!.firstChild! as HTMLElement;
522
- const blockContentBoundingBox = blockContent.getBoundingClientRect();
523
-
524
567
  return {
525
568
  block: this.editor.getBlock(this.hoveredBlock!.getAttribute("data-id")!)!,
526
- referenceRect: new DOMRect(
527
- this.horizontalPosAnchoredAtRoot
528
- ? this.horizontalPosAnchor
529
- : blockContentBoundingBox.x,
530
- blockContentBoundingBox.y,
531
- blockContentBoundingBox.width,
532
- blockContentBoundingBox.height
533
- ),
534
569
  };
535
570
  }
536
571
  }
@@ -4,15 +4,13 @@ import { BlockSchema } from "../Blocks/api/blockTypes";
4
4
 
5
5
  export type FormattingToolbarStaticParams<BSchema extends BlockSchema> = {
6
6
  editor: BlockNoteEditor<BSchema>;
7
- };
8
7
 
9
- export type FormattingToolbarDynamicParams = {
10
- referenceRect: DOMRect;
8
+ getReferenceRect: () => DOMRect;
11
9
  };
12
10
 
13
- export type FormattingToolbar = EditorElement<
14
- FormattingToolbarDynamicParams
15
- >;
11
+ export type FormattingToolbarDynamicParams = {};
12
+
13
+ export type FormattingToolbar = EditorElement<FormattingToolbarDynamicParams>;
16
14
  export type FormattingToolbarFactory<BSchema extends BlockSchema> =
17
15
  ElementFactory<
18
16
  FormattingToolbarStaticParams<BSchema>,
@@ -9,7 +9,6 @@ import { EditorView } from "prosemirror-view";
9
9
  import { BlockNoteEditor, BlockSchema } from "../..";
10
10
  import {
11
11
  FormattingToolbar,
12
- FormattingToolbarDynamicParams,
13
12
  FormattingToolbarFactory,
14
13
  FormattingToolbarStaticParams,
15
14
  } from "./FormattingToolbarFactoryTypes";
@@ -44,6 +43,8 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
44
43
 
45
44
  public prevWasEditable: boolean | null = null;
46
45
 
46
+ private lastPosition: DOMRect | undefined;
47
+
47
48
  public shouldShow: (props: {
48
49
  view: EditorView;
49
50
  state: EditorState;
@@ -80,8 +81,6 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
80
81
 
81
82
  this.ttEditor.on("focus", this.focusHandler);
82
83
  this.ttEditor.on("blur", this.blurHandler);
83
-
84
- document.addEventListener("scroll", this.scrollHandler);
85
84
  }
86
85
 
87
86
  viewMousedownHandler = () => {
@@ -110,11 +109,15 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
110
109
  return;
111
110
  }
112
111
 
112
+ // Checks if the focus is moving to an element outside the editor. If it is,
113
+ // the toolbar is hidden.
113
114
  if (
114
- event?.relatedTarget &&
115
- this.formattingToolbar.element?.parentNode?.contains(
116
- event.relatedTarget as Node
117
- )
115
+ // An element is clicked.
116
+ event &&
117
+ event.relatedTarget &&
118
+ // Element is outside the toolbar.
119
+ (this.formattingToolbar.element === (event.relatedTarget as Node) ||
120
+ this.formattingToolbar.element?.contains(event.relatedTarget as Node))
118
121
  ) {
119
122
  return;
120
123
  }
@@ -125,12 +128,6 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
125
128
  }
126
129
  };
127
130
 
128
- scrollHandler = () => {
129
- if (this.toolbarIsOpen) {
130
- this.formattingToolbar.render(this.getDynamicParams(), false);
131
- }
132
- };
133
-
134
131
  update(view: EditorView, oldState?: EditorState) {
135
132
  const { state, composing } = view;
136
133
  const { doc, selection } = state;
@@ -166,15 +163,9 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
166
163
  !this.preventShow &&
167
164
  (shouldShow || this.preventHide)
168
165
  ) {
169
- this.formattingToolbar.render(this.getDynamicParams(), true);
166
+ this.formattingToolbar.render({}, true);
170
167
  this.toolbarIsOpen = true;
171
168
 
172
- // TODO: Is this necessary? Also for other menu plugins.
173
- // Listener stops focus moving to the menu on click.
174
- this.formattingToolbar.element!.addEventListener("mousedown", (event) =>
175
- event.preventDefault()
176
- );
177
-
178
169
  return;
179
170
  }
180
171
 
@@ -184,7 +175,7 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
184
175
  !this.preventShow &&
185
176
  (shouldShow || this.preventHide)
186
177
  ) {
187
- this.formattingToolbar.render(this.getDynamicParams(), false);
178
+ this.formattingToolbar.render({}, false);
188
179
  return;
189
180
  }
190
181
 
@@ -197,12 +188,6 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
197
188
  this.formattingToolbar.hide();
198
189
  this.toolbarIsOpen = false;
199
190
 
200
- // Listener stops focus moving to the menu on click.
201
- this.formattingToolbar.element!.removeEventListener(
202
- "mousedown",
203
- (event) => event.preventDefault()
204
- );
205
-
206
191
  return;
207
192
  }
208
193
  }
@@ -214,8 +199,6 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
214
199
 
215
200
  this.ttEditor.off("focus", this.focusHandler);
216
201
  this.ttEditor.off("blur", this.blurHandler);
217
-
218
- document.removeEventListener("scroll", this.scrollHandler);
219
202
  }
220
203
 
221
204
  getSelectionBoundingBox() {
@@ -241,12 +224,22 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
241
224
  getStaticParams(): FormattingToolbarStaticParams<BSchema> {
242
225
  return {
243
226
  editor: this.editor,
244
- };
245
- }
246
-
247
- getDynamicParams(): FormattingToolbarDynamicParams {
248
- return {
249
- referenceRect: this.getSelectionBoundingBox(),
227
+ getReferenceRect: () => {
228
+ if (!this.toolbarIsOpen) {
229
+ if (this.lastPosition === undefined) {
230
+ throw new Error(
231
+ "Attempted to access selection reference rect before rendering formatting toolbar."
232
+ );
233
+ }
234
+
235
+ return this.lastPosition;
236
+ }
237
+
238
+ const selectionBoundingBox = this.getSelectionBoundingBox();
239
+ this.lastPosition = selectionBoundingBox;
240
+
241
+ return selectionBoundingBox;
242
+ },
250
243
  };
251
244
  }
252
245
  }
@@ -3,13 +3,13 @@ import { EditorElement, ElementFactory } from "../../shared/EditorElement";
3
3
  export type HyperlinkToolbarStaticParams = {
4
4
  editHyperlink: (url: string, text: string) => void;
5
5
  deleteHyperlink: () => void;
6
+
7
+ getReferenceRect: () => DOMRect;
6
8
  };
7
9
 
8
10
  export type HyperlinkToolbarDynamicParams = {
9
11
  url: string;
10
12
  text: string;
11
-
12
- referenceRect: DOMRect;
13
13
  };
14
14
 
15
15
  export type HyperlinkToolbar = EditorElement<HyperlinkToolbarDynamicParams>;