Skip to content

Commit 64b47d0

Browse files
fix: useEditorDOMElement hook (#2619)
1 parent 46355c0 commit 64b47d0

11 files changed

Lines changed: 79 additions & 32 deletions

File tree

packages/core/src/editor/BlockNoteEditor.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,13 @@ export class BlockNoteEditor<
562562
};
563563
this.pmSchema.cached.blockNoteEditor = this;
564564

565+
this._tiptapEditor.on("mount", () => {
566+
this.headless = false;
567+
});
568+
this._tiptapEditor.on("unmount", () => {
569+
this.headless = true;
570+
});
571+
565572
// Initialize managers
566573
this._blockManager = new BlockManager(this as any);
567574

@@ -758,9 +765,7 @@ export class BlockNoteEditor<
758765
return this.prosemirrorView?.hasFocus() || false;
759766
}
760767

761-
public get headless() {
762-
return !this._tiptapEditor.isInitialized;
763-
}
768+
public headless = true;
764769

765770
/**
766771
* Focus on the editor
@@ -1296,7 +1301,7 @@ export class BlockNoteEditor<
12961301
editor: BlockNoteEditor<BSchema, ISchema, SSchema>;
12971302
}) => void,
12981303
) {
1299-
this._eventManager.onMount(callback);
1304+
return this._eventManager.onMount(callback);
13001305
}
13011306

13021307
/**
@@ -1312,7 +1317,7 @@ export class BlockNoteEditor<
13121317
editor: BlockNoteEditor<BSchema, ISchema, SSchema>;
13131318
}) => void,
13141319
) {
1315-
this._eventManager.onUnmount(callback);
1320+
return this._eventManager.onUnmount(callback);
13161321
}
13171322

13181323
/**

packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515

1616
import { useComponentsContext } from "../../../editor/ComponentsContext.js";
1717
import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js";
18+
import { useEditorDOMElement } from "../../../hooks/useEditorDomElement.js";
1819
import { useEditorState } from "../../../hooks/useEditorState.js";
1920
import { useExtension } from "../../../hooks/useExtension.js";
2021
import { useDictionary } from "../../../i18n/dictionary.js";
@@ -41,6 +42,7 @@ function checkLinkInSchema(
4142

4243
export const CreateLinkButton = () => {
4344
const editor = useBlockNoteEditor<any, any, any>();
45+
const editorDOMElement = useEditorDOMElement();
4446
const Components = useComponentsContext()!;
4547
const dict = useDictionary();
4648

@@ -97,13 +99,12 @@ export const CreateLinkButton = () => {
9799
}
98100
};
99101

100-
const domElement = editor.domElement;
101-
domElement?.addEventListener("keydown", callback);
102+
editorDOMElement?.addEventListener("keydown", callback);
102103

103104
return () => {
104-
domElement?.removeEventListener("keydown", callback);
105+
editorDOMElement?.removeEventListener("keydown", callback);
105106
};
106-
}, [editor.domElement]);
107+
}, [editorDOMElement]);
107108

108109
if (state === undefined) {
109110
return null;

packages/react/src/components/LinkToolbar/LinkToolbarController.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Range } from "@tiptap/core";
44
import { FC, useEffect, useMemo, useState } from "react";
55

66
import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
7+
import { useEditorDOMElement } from "../../hooks/useEditorDomElement.js";
78
import { useExtension } from "../../hooks/useExtension.js";
89
import { FloatingUIOptions } from "../Popovers/FloatingUIOptions.js";
910
import {
@@ -22,6 +23,8 @@ export const LinkToolbarController = (props: {
2223
const [toolbarOpen, setToolbarOpen] = useState(false);
2324
const [toolbarPositionFrozen, setToolbarPositionFrozen] = useState(false);
2425

26+
const editorDOMElement = useEditorDOMElement();
27+
2528
const linkToolbar = useExtension(LinkToolbarExtension);
2629

2730
// Because the toolbar opens with a delay when a link is hovered by the mouse
@@ -98,16 +101,14 @@ export const LinkToolbarController = (props: {
98101
const destroyOnSelectionChangeHandler =
99102
editor.onSelectionChange(textCursorCallback);
100103

101-
const domElement = editor.domElement;
102-
103-
domElement?.addEventListener("mouseover", mouseCursorCallback);
104+
editorDOMElement?.addEventListener("mouseover", mouseCursorCallback);
104105

105106
return () => {
106107
destroyOnChangeHandler();
107108
destroyOnSelectionChangeHandler();
108-
domElement?.removeEventListener("mouseover", mouseCursorCallback);
109+
editorDOMElement?.removeEventListener("mouseover", mouseCursorCallback);
109110
};
110-
}, [editor, editor.domElement, linkToolbar, link, toolbarPositionFrozen]);
111+
}, [editor, editorDOMElement, linkToolbar, link, toolbarPositionFrozen]);
111112

112113
const floatingUIOptions = useMemo<FloatingUIOptions>(
113114
() => ({

packages/react/src/components/Popovers/PositionPopover.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { posToDOMRect } from "@tiptap/core";
22
import { ReactNode, useMemo } from "react";
33

44
import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
5+
import { useEditorDOMElement } from "../../hooks/useEditorDomElement.js";
56
import { FloatingUIOptions } from "./FloatingUIOptions.js";
67
import { GenericPopover, GenericPopoverReference } from "./GenericPopover.js";
78

@@ -15,6 +16,7 @@ export const PositionPopover = (
1516
const { from, to } = position || {};
1617

1718
const editor = useBlockNoteEditor<any, any, any>();
19+
const editorDOMElement = useEditorDOMElement();
1820

1921
const reference = useMemo<GenericPopoverReference | undefined>(() => {
2022
if (from === undefined || to === undefined) {
@@ -25,11 +27,11 @@ export const PositionPopover = (
2527
// Use first child as the editor DOM element may itself be scrollable.
2628
// For FloatingUI to auto-update the position during scrolling, the
2729
// `contextElement` must be a descendant of the scroll container.
28-
element: editor.domElement?.firstElementChild || undefined,
30+
element: editorDOMElement?.firstElementChild || undefined,
2931
getBoundingClientRect: () =>
3032
posToDOMRect(editor.prosemirrorView, from, to ?? from),
3133
};
32-
}, [editor, from, to]);
34+
}, [editor, editorDOMElement, from, to]);
3335

3436
return (
3537
<GenericPopover reference={reference} {...floatingUIOptions}>

packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { autoPlacement, offset, shift, size } from "@floating-ui/react";
77
import { FC, useEffect, useMemo } from "react";
88

99
import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js";
10+
import { useEditorDOMElement } from "../../../hooks/useEditorDomElement.js";
1011
import {
1112
useExtension,
1213
useExtensionState,
@@ -64,6 +65,7 @@ export function GridSuggestionMenuController<
6465
InlineContentSchema,
6566
StyleSchema
6667
>();
68+
const editorDOMElement = useEditorDOMElement();
6769

6870
const {
6971
triggerCharacter,
@@ -108,7 +110,7 @@ export function GridSuggestionMenuController<
108110
// Use first child as the editor DOM element may itself be scrollable.
109111
// For FloatingUI to auto-update the position during scrolling, the
110112
// `contextElement` must be a descendant of the scroll container.
111-
element: (editor.domElement?.firstChild || undefined) as
113+
element: (editorDOMElement?.firstChild || undefined) as
112114
| Element
113115
| undefined,
114116
getBoundingClientRect: () => state?.referencePos || new DOMRect(),

packages/react/src/components/SuggestionMenu/GridSuggestionMenu/hooks/useGridSuggestionMenuKeyboardNavigation.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BlockNoteEditor } from "@blocknote/core";
22
import { useEffect, useState } from "react";
3+
import { useEditorDOMElement } from "../../../../hooks/useEditorDomElement.js";
34

45
// Hook which handles keyboard navigation of a grid suggestion menu. Arrow keys
56
// are used to select a menu item, enter is used to execute it.
@@ -10,6 +11,7 @@ export function useGridSuggestionMenuKeyboardNavigation<Item>(
1011
columns: number,
1112
onItemClick?: (item: Item) => void,
1213
) {
14+
const editorDOMElement = useEditorDOMElement(editor);
1315
const [selectedIndex, setSelectedIndex] = useState<number>(0);
1416

1517
const isGrid = columns !== undefined && columns > 1;
@@ -66,17 +68,20 @@ export function useGridSuggestionMenuKeyboardNavigation<Item>(
6668
return false;
6769
};
6870

69-
const domElement = editor.domElement;
70-
domElement?.addEventListener("keydown", handleMenuNavigationKeys, true);
71+
editorDOMElement?.addEventListener(
72+
"keydown",
73+
handleMenuNavigationKeys,
74+
true,
75+
);
7176

7277
return () => {
73-
domElement?.removeEventListener(
78+
editorDOMElement?.removeEventListener(
7479
"keydown",
7580
handleMenuNavigationKeys,
7681
true,
7782
);
7883
};
79-
}, [editor.domElement, items, selectedIndex, onItemClick, columns, isGrid]);
84+
}, [editorDOMElement, items, selectedIndex, onItemClick, columns, isGrid]);
8085

8186
// Resets index when items change
8287
useEffect(() => {

packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { autoPlacement, offset, shift, size } from "@floating-ui/react";
88
import { FC, useEffect, useMemo } from "react";
99

1010
import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
11+
import { useEditorDOMElement } from "../../hooks/useEditorDomElement.js";
1112
import { useExtension, useExtensionState } from "../../hooks/useExtension.js";
1213
import { FloatingUIOptions } from "../Popovers/FloatingUIOptions.js";
1314
import {
@@ -58,6 +59,7 @@ export function SuggestionMenuController<
5859
InlineContentSchema,
5960
StyleSchema
6061
>();
62+
const editorDOMElement = useEditorDOMElement();
6163

6264
const {
6365
triggerCharacter,
@@ -101,7 +103,7 @@ export function SuggestionMenuController<
101103
// Use first child as the editor DOM element may itself be scrollable.
102104
// For FloatingUI to auto-update the position during scrolling, the
103105
// `contextElement` must be a descendant of the scroll container.
104-
element: (editor.domElement?.firstChild || undefined) as
106+
element: (editorDOMElement?.firstChild || undefined) as
105107
| Element
106108
| undefined,
107109
getBoundingClientRect: () => state?.referencePos || new DOMRect(),

packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
import { BlockNoteEditor } from "@blocknote/core";
22
import { useEffect } from "react";
3+
import { useEditorDOMElement } from "../../../hooks/useEditorDomElement.js";
34
import { useSuggestionMenuKeyboardHandler } from "./useSuggestionMenuKeyboardHandler.js";
45

56
// Hook which handles keyboard navigation of a suggestion menu. Up & down arrow
67
// keys are used to select a menu item, enter is used to execute it.
78
export function useSuggestionMenuKeyboardNavigation<Item>(
8-
editor: BlockNoteEditor<any, any, any>,
9+
_editor: BlockNoteEditor<any, any, any>,
910
query: string,
1011
items: Item[],
1112
onItemClick?: (item: Item) => void,
1213
element?: HTMLElement,
1314
) {
15+
const editorDOMElement = useEditorDOMElement();
1416
const { selectedIndex, setSelectedIndex, handler } =
1517
useSuggestionMenuKeyboardHandler(items, onItemClick);
1618

1719
useEffect(() => {
18-
const el = element || editor.domElement;
20+
const el = element || editorDOMElement;
1921
el?.addEventListener("keydown", handler, true);
2022

2123
return () => {
2224
el?.removeEventListener("keydown", handler, true);
2325
};
24-
}, [editor.domElement, items, selectedIndex, onItemClick, element, handler]);
26+
}, [editorDOMElement, items, selectedIndex, onItemClick, element, handler]);
2527

2628
// Resets index when items change
2729
useEffect(() => {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { BlockNoteEditor } from "@blocknote/core";
2+
3+
import { useBlockNoteContext } from "../editor/BlockNoteContext.js";
4+
import { useEditorState } from "./useEditorState.js";
5+
6+
// Returns the editor's DOM element reactively.
7+
export function useEditorDOMElement(editor?: BlockNoteEditor<any, any, any>) {
8+
const editorContext = useBlockNoteContext();
9+
if (!editor) {
10+
editor = editorContext?.editor;
11+
}
12+
13+
return useEditorState({
14+
editor,
15+
selector: (ctx) => ctx.editor?.domElement,
16+
equalityFn: (a, b) => a === b,
17+
on: "mount",
18+
});
19+
}

packages/react/src/hooks/useEditorState.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export type UseEditorStateOptions<
4646
* The event to subscribe to.
4747
* @default "all"
4848
*/
49-
on?: "all" | "selection" | "change";
49+
on?: "all" | "mount" | "selection" | "change";
5050
};
5151

5252
/**
@@ -117,7 +117,7 @@ class EditorStateManager<
117117
*/
118118
watch(
119119
nextEditor: BlockNoteEditor<any, any, any> | null,
120-
on: "all" | "selection" | "change",
120+
on: "all" | "mount" | "selection" | "change",
121121
): undefined | (() => void) {
122122
this.editor = nextEditor as TEditor;
123123

@@ -135,14 +135,21 @@ class EditorStateManager<
135135
const currentTiptapEditor = this.editor._tiptapEditor;
136136

137137
const EVENT_TYPES = {
138-
all: "transaction",
139-
selection: "selectionUpdate",
140-
change: "update",
138+
all: ["transaction", "create", "mount", "unmount"],
139+
// Listen for "create" as "mount" may fire before the hook is run.
140+
mount: ["create", "mount", "unmount"],
141+
selection: ["selectionUpdate"],
142+
change: ["update"],
141143
} as const;
142144

143-
currentTiptapEditor.on(EVENT_TYPES[on], fn);
145+
for (const eventType of EVENT_TYPES[on]) {
146+
currentTiptapEditor.on(eventType, fn);
147+
}
148+
144149
return () => {
145-
currentTiptapEditor.off(EVENT_TYPES[on], fn);
150+
for (const eventType of EVENT_TYPES[on]) {
151+
currentTiptapEditor.off(eventType, fn);
152+
}
146153
};
147154
}
148155

0 commit comments

Comments
 (0)