Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/core/src/editor/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1296,7 +1296,7 @@ export class BlockNoteEditor<
editor: BlockNoteEditor<BSchema, ISchema, SSchema>;
}) => void,
) {
this._eventManager.onMount(callback);
return this._eventManager.onMount(callback);
}

/**
Expand All @@ -1312,7 +1312,7 @@ export class BlockNoteEditor<
editor: BlockNoteEditor<BSchema, ISchema, SSchema>;
}) => void,
) {
this._eventManager.onUnmount(callback);
return this._eventManager.onUnmount(callback);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {

import { useComponentsContext } from "../../../editor/ComponentsContext.js";
import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js";
import { useEditorDOMElement } from "../../../hooks/useEditorDomElement.js";
import { useEditorState } from "../../../hooks/useEditorState.js";
import { useExtension } from "../../../hooks/useExtension.js";
import { useDictionary } from "../../../i18n/dictionary.js";
Expand All @@ -41,6 +42,7 @@ function checkLinkInSchema(

export const CreateLinkButton = () => {
const editor = useBlockNoteEditor<any, any, any>();
const editorDOMElement = useEditorDOMElement();
const Components = useComponentsContext()!;
const dict = useDictionary();

Expand Down Expand Up @@ -97,13 +99,12 @@ export const CreateLinkButton = () => {
}
};

const domElement = editor.domElement;
domElement?.addEventListener("keydown", callback);
editorDOMElement?.addEventListener("keydown", callback);

return () => {
domElement?.removeEventListener("keydown", callback);
editorDOMElement?.removeEventListener("keydown", callback);
};
}, [editor.domElement]);
}, [editorDOMElement]);

if (state === undefined) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Range } from "@tiptap/core";
import { FC, useEffect, useMemo, useState } from "react";

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

const editorDOMElement = useEditorDOMElement();

const linkToolbar = useExtension(LinkToolbarExtension);

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

const domElement = editor.domElement;

domElement?.addEventListener("mouseover", mouseCursorCallback);
editorDOMElement?.addEventListener("mouseover", mouseCursorCallback);

return () => {
destroyOnChangeHandler();
destroyOnSelectionChangeHandler();
domElement?.removeEventListener("mouseover", mouseCursorCallback);
editorDOMElement?.removeEventListener("mouseover", mouseCursorCallback);
};
}, [editor, editor.domElement, linkToolbar, link, toolbarPositionFrozen]);
}, [editor, editorDOMElement, linkToolbar, link, toolbarPositionFrozen]);

const floatingUIOptions = useMemo<FloatingUIOptions>(
() => ({
Expand Down
6 changes: 4 additions & 2 deletions packages/react/src/components/Popovers/PositionPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { posToDOMRect } from "@tiptap/core";
import { ReactNode, useMemo } from "react";

import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
import { useEditorDOMElement } from "../../hooks/useEditorDomElement.js";
import { FloatingUIOptions } from "./FloatingUIOptions.js";
import { GenericPopover, GenericPopoverReference } from "./GenericPopover.js";

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

const editor = useBlockNoteEditor<any, any, any>();
const editorDOMElement = useEditorDOMElement();

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

return (
<GenericPopover reference={reference} {...floatingUIOptions}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { autoPlacement, offset, shift, size } from "@floating-ui/react";
import { FC, useEffect, useMemo } from "react";

import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js";
import { useEditorDOMElement } from "../../../hooks/useEditorDomElement.js";
import {
useExtension,
useExtensionState,
Expand Down Expand Up @@ -64,6 +65,7 @@ export function GridSuggestionMenuController<
InlineContentSchema,
StyleSchema
>();
const editorDOMElement = useEditorDOMElement();

const {
triggerCharacter,
Expand Down Expand Up @@ -108,7 +110,7 @@ export function GridSuggestionMenuController<
// Use first child as the editor DOM element may itself be scrollable.
// For FloatingUI to auto-update the position during scrolling, the
// `contextElement` must be a descendant of the scroll container.
element: (editor.domElement?.firstChild || undefined) as
element: (editorDOMElement?.firstChild || undefined) as
| Element
| undefined,
getBoundingClientRect: () => state?.referencePos || new DOMRect(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BlockNoteEditor } from "@blocknote/core";
import { useEffect, useState } from "react";
import { useEditorDOMElement } from "../../../../hooks/useEditorDomElement.js";

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

const isGrid = columns !== undefined && columns > 1;
Expand Down Expand Up @@ -66,17 +68,20 @@ export function useGridSuggestionMenuKeyboardNavigation<Item>(
return false;
};

const domElement = editor.domElement;
domElement?.addEventListener("keydown", handleMenuNavigationKeys, true);
editorDOMElement?.addEventListener(
"keydown",
handleMenuNavigationKeys,
true,
);

return () => {
domElement?.removeEventListener(
editorDOMElement?.removeEventListener(
"keydown",
handleMenuNavigationKeys,
true,
);
};
}, [editor.domElement, items, selectedIndex, onItemClick, columns, isGrid]);
}, [editorDOMElement, items, selectedIndex, onItemClick, columns, isGrid]);

// Resets index when items change
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { autoPlacement, offset, shift, size } from "@floating-ui/react";
import { FC, useEffect, useMemo } from "react";

import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
import { useEditorDOMElement } from "../../hooks/useEditorDomElement.js";
import { useExtension, useExtensionState } from "../../hooks/useExtension.js";
import { FloatingUIOptions } from "../Popovers/FloatingUIOptions.js";
import {
Expand Down Expand Up @@ -58,6 +59,7 @@ export function SuggestionMenuController<
InlineContentSchema,
StyleSchema
>();
const editorDOMElement = useEditorDOMElement();

const {
triggerCharacter,
Expand Down Expand Up @@ -101,7 +103,7 @@ export function SuggestionMenuController<
// Use first child as the editor DOM element may itself be scrollable.
// For FloatingUI to auto-update the position during scrolling, the
// `contextElement` must be a descendant of the scroll container.
element: (editor.domElement?.firstChild || undefined) as
element: (editorDOMElement?.firstChild || undefined) as
| Element
| undefined,
getBoundingClientRect: () => state?.referencePos || new DOMRect(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import { BlockNoteEditor } from "@blocknote/core";
import { useEffect } from "react";
import { useEditorDOMElement } from "../../../hooks/useEditorDomElement.js";
import { useSuggestionMenuKeyboardHandler } from "./useSuggestionMenuKeyboardHandler.js";

// Hook which handles keyboard navigation of a suggestion menu. Up & down arrow
// keys are used to select a menu item, enter is used to execute it.
export function useSuggestionMenuKeyboardNavigation<Item>(
editor: BlockNoteEditor<any, any, any>,
_editor: BlockNoteEditor<any, any, any>,
query: string,
items: Item[],
onItemClick?: (item: Item) => void,
element?: HTMLElement,
) {
const editorDOMElement = useEditorDOMElement();
const { selectedIndex, setSelectedIndex, handler } =
useSuggestionMenuKeyboardHandler(items, onItemClick);

useEffect(() => {
const el = element || editor.domElement;
const el = element || editorDOMElement;
el?.addEventListener("keydown", handler, true);

return () => {
el?.removeEventListener("keydown", handler, true);
};
}, [editor.domElement, items, selectedIndex, onItemClick, element, handler]);
}, [editorDOMElement, items, selectedIndex, onItemClick, element, handler]);

// Resets index when items change
useEffect(() => {
Expand Down
47 changes: 47 additions & 0 deletions packages/react/src/hooks/useEditorDomElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { BlockNoteEditor } from "@blocknote/core";
import { useEffect, useState } from "react";

import { useBlockNoteContext } from "../editor/BlockNoteContext.js";
import { useEditorState } from "./useEditorState.js";

/**
* Returns the editor's DOM element reactively.
*
* `editor.domElement` is gated behind TipTap's `isInitialized` flag, which is
* set in a `setTimeout(0)` after mount. A plain read of `editor.domElement`
* during the first render (or effect) will see `undefined`, and no transaction
* fires after `isInitialized` flips to notify subscribers.
*
* This hook uses `useEditorState` to pick up changes that coincide with
* transactions (e.g. remounts), and supplements it with a TipTap `create`
* event listener to handle the initial mount timing.
*/
export function useEditorDOMElement(editor?: BlockNoteEditor<any, any, any>) {
const editorContext = useBlockNoteContext();
if (!editor) {
editor = editorContext?.editor;
}

// Handle initial mount timing. TipTap sets isInitialized synchronously
// right after emitting the "create" event, so by the time React processes
// this state update, editor.domElement is available.
const [, bumpMountVersion] = useState(0);
useEffect(() => {
if (!editor?.headless) {
return;
}
const handler = () => bumpMountVersion((v) => v + 1);
editor?._tiptapEditor.on("create", handler);
return () => {
editor?._tiptapEditor.off("create", handler);
};
}, [editor]);

// Re-evaluate editor.domElement on every render (including the one
// triggered by setInitialized above) and on every transaction.
return useEditorState({
editor,
selector: (ctx) => ctx.editor?.domElement,
equalityFn: (a, b) => a === b,
});
}
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export * from "./hooks/useActiveStyles.js";
export * from "./hooks/useBlockNoteEditor.js";
export * from "./hooks/useCreateBlockNote.js";
export * from "./hooks/useEditorChange.js";
export * from "./hooks/useEditorDomElement.js";
export * from "./hooks/useEditorSelectionBoundingBox.js";
export * from "./hooks/useEditorSelectionChange.js";
export * from "./hooks/useFocusWithin.js";
Expand Down
Loading