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
15 changes: 10 additions & 5 deletions packages/core/src/editor/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,13 @@ export class BlockNoteEditor<
};
this.pmSchema.cached.blockNoteEditor = this;

this._tiptapEditor.on("mount", () => {
this.headless = false;
});
this._tiptapEditor.on("unmount", () => {
this.headless = true;
});

// Initialize managers
this._blockManager = new BlockManager(this as any);

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

public get headless() {
return !this._tiptapEditor.isInitialized;
}
public headless = true;

/**
* Focus on the editor
Expand Down Expand Up @@ -1296,7 +1301,7 @@ export class BlockNoteEditor<
editor: BlockNoteEditor<BSchema, ISchema, SSchema>;
}) => void,
) {
this._eventManager.onMount(callback);
return this._eventManager.onMount(callback);
}

/**
Expand All @@ -1312,7 +1317,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
19 changes: 19 additions & 0 deletions packages/react/src/hooks/useEditorDomElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { BlockNoteEditor } from "@blocknote/core";

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

// Returns the editor's DOM element reactively.
export function useEditorDOMElement(editor?: BlockNoteEditor<any, any, any>) {
const editorContext = useBlockNoteContext();
if (!editor) {
editor = editorContext?.editor;
}

return useEditorState({
editor,
selector: (ctx) => ctx.editor?.domElement,
equalityFn: (a, b) => a === b,
on: "mount",
});
}
21 changes: 14 additions & 7 deletions packages/react/src/hooks/useEditorState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export type UseEditorStateOptions<
* The event to subscribe to.
* @default "all"
*/
on?: "all" | "selection" | "change";
on?: "all" | "mount" | "selection" | "change";
};

/**
Expand Down Expand Up @@ -117,7 +117,7 @@ class EditorStateManager<
*/
watch(
nextEditor: BlockNoteEditor<any, any, any> | null,
on: "all" | "selection" | "change",
on: "all" | "mount" | "selection" | "change",
): undefined | (() => void) {
this.editor = nextEditor as TEditor;

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

const EVENT_TYPES = {
all: "transaction",
selection: "selectionUpdate",
change: "update",
all: ["transaction", "create", "mount", "unmount"],
// Listen for "create" as "mount" may fire before the hook is run.
mount: ["create", "mount", "unmount"],
selection: ["selectionUpdate"],
change: ["update"],
} as const;

currentTiptapEditor.on(EVENT_TYPES[on], fn);
for (const eventType of EVENT_TYPES[on]) {
currentTiptapEditor.on(eventType, fn);
}

return () => {
currentTiptapEditor.off(EVENT_TYPES[on], fn);
for (const eventType of EVENT_TYPES[on]) {
currentTiptapEditor.off(eventType, fn);
}
};
}

Expand Down
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