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
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ export const FilePanelController = (props: {
const Component = props.filePanel || FilePanel;

return (
<BlockPopover blockId={blockId} {...floatingUIOptions}>
<BlockPopover
blockId={blockId}
includeNestedBlocks={false}
{...floatingUIOptions}
>
{blockId && <Component blockId={blockId} />}
</BlockPopover>
);
Expand Down
50 changes: 43 additions & 7 deletions packages/react/src/components/Popovers/BlockPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { GenericPopover, GenericPopoverReference } from "./GenericPopover.js";
export const BlockPopover = (
props: FloatingUIOptions & {
blockId: string | undefined;
ignoreNestingOffset?: boolean;
includeNestedBlocks?: boolean;
children: ReactNode;
},
) => {
Expand All @@ -28,18 +30,52 @@ export const BlockPopover = (
return undefined;
}

const { node } = editor.prosemirrorView.domAtPos(
const blockElement = editor.prosemirrorView.domAtPos(
nodePosInfo.posBeforeNode + 1,
);
if (!(node instanceof Element)) {
).node;
if (!(blockElement instanceof Element)) {
return undefined;
}

return {
element: node,
};
const blockContentElement = blockElement.firstElementChild;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I like the use of firstElementChild. It assumes a specific dom structure to get the dom element - which is not typesafe / futureproof.

Better approach would be to use the methods we have like getblockinfo to get the content block, and then call getdomatpos for it

(example; the current approach would probably break for column blocks which don't have blockContent)

if (!(blockContentElement instanceof Element)) {
return undefined;
}

const element =
props.includeNestedBlocks === false
? blockContentElement
: blockElement;

if (props.ignoreNestingOffset) {
return {
element,
getBoundingClientRect: () => {
const boundingClientRect = element.getBoundingClientRect();

const outerBlockGroupElement = element.closest(
".bn-editor > .bn-block-group > .bn-block-outer, .bn-block-column > .bn-block-outer",
);
if (!(outerBlockGroupElement instanceof Element)) {
return undefined;
}

const outerBlockGroupBoundingClientRect =
outerBlockGroupElement.getBoundingClientRect();

return new DOMRect(
outerBlockGroupBoundingClientRect.x,
boundingClientRect.y,
outerBlockGroupBoundingClientRect.width,
boundingClientRect.height,
);
},
};
}

return { element };
}),
[editor, blockId],
[editor, blockId, props.includeNestedBlocks, props.ignoreNestingOffset],
);

return (
Expand Down
40 changes: 2 additions & 38 deletions packages/react/src/components/SideMenu/SideMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { SideMenuExtension } from "@blocknote/core/extensions";
import { ReactNode, useMemo } from "react";
import { ReactNode } from "react";

import { useComponentsContext } from "../../editor/ComponentsContext.js";
import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
import { useExtensionState } from "../../hooks/useExtension.js";
import { AddBlockButton } from "./DefaultButtons/AddBlockButton.js";
import { DragHandleButton } from "./DefaultButtons/DragHandleButton.js";
import { SideMenuProps } from "./SideMenuProps.js";
Expand All @@ -20,41 +17,8 @@ import { SideMenuProps } from "./SideMenuProps.js";
export const SideMenu = (props: SideMenuProps & { children?: ReactNode }) => {
const Components = useComponentsContext()!;

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

const block = useExtensionState(SideMenuExtension, {
editor,
selector: (state) => state?.block,
});

const dataAttributes = useMemo(() => {
if (block === undefined) {
return {};
}

const attrs: Record<string, string> = {
"data-block-type": block.type,
};

if (block.type === "heading") {
attrs["data-level"] = (block.props as any).level.toString();
}

if (
editor.schema.blockSpecs[block.type].implementation.meta?.fileBlockAccept
) {
if (block.props.url) {
attrs["data-url"] = "true";
} else {
attrs["data-url"] = "false";
}
}

return attrs;
}, [block, editor.schema.blockSpecs]);

return (
<Components.SideMenu.Root className={"bn-side-menu"} {...dataAttributes}>
<Components.SideMenu.Root className={"bn-side-menu"}>
{props.children || (
<>
<AddBlockButton />
Expand Down
128 changes: 126 additions & 2 deletions packages/react/src/components/SideMenu/SideMenuController.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core";
import { SideMenuExtension } from "@blocknote/core/extensions";
import { size } from "@floating-ui/react";
import { FC, useMemo } from "react";

import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
import { useExtensionState } from "../../hooks/useExtension.js";
import { BlockPopover } from "../Popovers/BlockPopover.js";
import { FloatingUIOptions } from "../Popovers/FloatingUIOptions.js";
Expand All @@ -11,6 +14,12 @@ export const SideMenuController = (props: {
sideMenu?: FC<SideMenuProps>;
floatingUIOptions?: Partial<FloatingUIOptions>;
}) => {
const editor = useBlockNoteEditor<
BlockSchema,
InlineContentSchema,
StyleSchema
>();

const state = useExtensionState(SideMenuExtension, {
selector: (state) => {
return state !== undefined
Expand All @@ -29,6 +38,117 @@ export const SideMenuController = (props: {
useFloatingOptions: {
open: show,
placement: "left-start",
middleware: [
// Adjusts the side menu height to align it vertically with the
// block's content. In some cases, like file blocks with captions,
// the height and top offset is adjusted to align it with a specific
// element within the block's content instead.
size({
apply({ elements }) {
// TODO: Need to fetch the block from extension, else it's
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code is called in useMemo, so the apply function only gets recreated when the deps array changes

// always `undefined` for some reason? Shouldn't the `apply`
// function get recreated with the updated `block` object each
// time it changes?
const block =
editor.getExtension(SideMenuExtension)?.store.state?.block;
if (block === undefined) {
return;
}

const blockElement =
elements.reference instanceof Element
? elements.reference
: elements.reference.contextElement;
if (blockElement === undefined) {
return;
}

const blockContentElement =
blockElement.querySelector(".bn-block-content");
if (blockContentElement === null) {
return;
}

const blockContentBoundingClientRect =
blockContentElement.getBoundingClientRect();

// Special case for file blocks with captions. In this case, we
// align the side menu with the first sibling of the file caption
// element.
const fileCaptionParentElement =
blockContentElement.querySelector(".bn-file-caption")
?.parentElement || null;
if (fileCaptionParentElement !== null) {
const fileElement = fileCaptionParentElement.firstElementChild;
if (fileElement) {
const fileBoundingClientRect =
fileElement.getBoundingClientRect();

elements.floating.style.setProperty(
"height",
`${fileBoundingClientRect.height}px`,
);
elements.floating.style.setProperty(
"top",
`${fileBoundingClientRect.y - blockContentBoundingClientRect.y}px`,
);

return;
}
}

// Special case for toggleable blocks. In this case, we align the
// side menu with the element containing the toggle button and
// rich text.
const toggleWrapperElement =
blockContentElement.querySelector(".bn-toggle-wrapper");
if (toggleWrapperElement !== null) {
const toggleWrapperBoundingClientRect =
toggleWrapperElement.getBoundingClientRect();

elements.floating.style.setProperty(
"height",
`${toggleWrapperBoundingClientRect.height}px`,
);
elements.floating.style.setProperty(
"top",
`${toggleWrapperBoundingClientRect.y - blockContentBoundingClientRect.y}px`,
);

return;
}

// Special case for table blocks. In this case, we align the side
// menu with the table element inside the block.
const tableElement = blockContentElement.querySelector(
".tableWrapper table",
);
if (tableElement !== null) {
const tableBoundingClientRect =
tableElement.getBoundingClientRect();

elements.floating.style.setProperty(
"height",
`${tableBoundingClientRect.height}px`,
);
elements.floating.style.setProperty(
"top",
`${tableBoundingClientRect.y - blockContentBoundingClientRect.y}px`,
);

return;
}

// Regular case, in which the side menu is aligned with the block
// content element.
elements.floating.style.setProperty(
"height",
`${blockContentBoundingClientRect.height}px`,
);
elements.floating.style.setProperty("top", "0");
},
}),
],
},
useDismissProps: {
enabled: false,
Expand All @@ -40,13 +160,17 @@ export const SideMenuController = (props: {
},
...props.floatingUIOptions,
}),
[props.floatingUIOptions, show],
[editor, props.floatingUIOptions, show],
);

const Component = props.sideMenu || SideMenu;

return (
<BlockPopover blockId={show ? block?.id : undefined} {...floatingUIOptions}>
<BlockPopover
blockId={show ? block?.id : undefined}
ignoreNestingOffset={true}
{...floatingUIOptions}
>
{block?.id && <Component />}
</BlockPopover>
);
Expand Down
29 changes: 0 additions & 29 deletions packages/react/src/editor/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -237,35 +237,6 @@ inline styles, it is added to the base z-index. */
--bn-ui-base-z-index: 0;
}

/* Matches Side Menu height to block line height */
.bn-side-menu {
height: 30px;
}

.bn-side-menu[data-block-type="heading"][data-level="1"] {
height: 78px;
}

.bn-side-menu[data-block-type="heading"][data-level="2"] {
height: 54px;
}

.bn-side-menu[data-block-type="heading"][data-level="3"] {
height: 37px;
}

.bn-side-menu[data-block-type="file"] {
height: 38px;
}

.bn-side-menu[data-block-type="audio"] {
height: 60px;
}

.bn-side-menu[data-url="false"] {
height: 54px;
}

/* Thread sidebar styling */
.bn-threads-sidebar {
border-radius: var(--bn-border-radius-medium);
Expand Down
Loading