Skip to content

Commit 1073248

Browse files
committed
Refactored formatting toolbar, file panel, and suggestion menu controllers
1 parent 9b266e5 commit 1073248

File tree

13 files changed

+414
-240
lines changed

13 files changed

+414
-240
lines changed

packages/core/src/blocks/File/helpers/render/createAddFileButton.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
2+
import { FilePanelPlugin } from "../../../../extensions/FilePanel/FilePanelPlugin.js";
23
import {
34
BlockConfig,
45
BlockFromConfigNoChildren,
@@ -36,11 +37,7 @@ export const createAddFileButton = (
3637
};
3738
// Opens the file toolbar.
3839
const addFileButtonClickHandler = () => {
39-
editor.transact((tr) =>
40-
tr.setMeta(editor.filePanel!.plugins[0], {
41-
block: block,
42-
}),
43-
);
40+
editor.getExtension(FilePanelPlugin)?.showMenu(block.id);
4441
};
4542
addFileButton.addEventListener(
4643
"mousedown",

packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts

Lines changed: 11 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
import { isNodeSelection, posToDOMRect } from "@tiptap/core";
21
import { Plugin, PluginKey } from "@tiptap/pm/state";
32
import {
43
createExtension,
54
createStore,
65
} from "../../editor/BlockNoteExtension.js";
76

87
export const FormattingToolbarExtension = createExtension((editor) => {
9-
const store = createStore({
10-
show: false,
11-
referencePos: null as DOMRect | null,
12-
});
13-
14-
let preventShow = false;
8+
const store = createStore({ show: false });
159

1610
return {
1711
key: "formattingToolbar",
@@ -22,32 +16,14 @@ export const FormattingToolbarExtension = createExtension((editor) => {
2216
props: {
2317
handleKeyDown: (_view, event) => {
2418
if (event.key === "Escape" && store.state.show) {
25-
store.setState({ show: false, referencePos: null });
19+
store.setState({ show: false });
2620
return true;
2721
}
2822
return false;
2923
},
3024
},
3125
}),
3226
],
33-
// TODO should go into core, perhaps `editor.getSelection().getBoundingBox()`
34-
getSelectionBoundingBox() {
35-
const { selection } = editor.prosemirrorState;
36-
37-
// support for CellSelections
38-
const { ranges } = selection;
39-
const from = Math.min(...ranges.map((range) => range.$from.pos));
40-
const to = Math.max(...ranges.map((range) => range.$to.pos));
41-
42-
if (isNodeSelection(selection)) {
43-
const node = editor.prosemirrorView.nodeDOM(from) as HTMLElement;
44-
if (node) {
45-
return node.getBoundingClientRect();
46-
}
47-
}
48-
49-
return posToDOMRect(editor.prosemirrorView, from, to);
50-
},
5127
init({ dom }) {
5228
const isElementWithinEditorWrapper = (element: Node | null) => {
5329
if (!element) {
@@ -63,50 +39,36 @@ export const FormattingToolbarExtension = createExtension((editor) => {
6339

6440
function onMouseDownHandler(e: MouseEvent) {
6541
if (!isElementWithinEditorWrapper(e.target as Node) || e.button === 0) {
66-
preventShow = true;
42+
store.setState({ show: false });
6743
}
6844
}
6945

7046
function onMouseUpHandler() {
71-
if (preventShow) {
72-
preventShow = false;
73-
setTimeout(() =>
74-
store.setState((prev) => ({
75-
...prev,
76-
show: true,
77-
referencePos: null,
78-
})),
79-
);
80-
}
47+
setTimeout(() => {
48+
if (editor.prosemirrorState.selection.empty) {
49+
store.setState({ show: false });
50+
} else {
51+
store.setState({ show: true });
52+
}
53+
}, 1);
8154
}
8255

8356
function onDragHandler() {
8457
if (store.state.show) {
85-
store.setState({ show: false, referencePos: null });
58+
store.setState({ show: false });
8659
}
8760
}
8861

89-
const onScrollHandler = () => {
90-
if (store.state.show) {
91-
store.setState((prev) => ({
92-
...prev,
93-
referencePos: this.getSelectionBoundingBox(),
94-
}));
95-
}
96-
};
97-
9862
dom.addEventListener("mousedown", onMouseDownHandler);
9963
dom.addEventListener("mouseup", onMouseUpHandler);
10064
dom.addEventListener("dragstart", onDragHandler);
10165
dom.addEventListener("dragover", onDragHandler);
102-
dom.addEventListener("scroll", onScrollHandler);
10366

10467
return () => {
10568
dom.removeEventListener("mousedown", onMouseDownHandler);
10669
dom.removeEventListener("mouseup", onMouseUpHandler);
10770
dom.removeEventListener("dragstart", onDragHandler);
10871
dom.removeEventListener("dragover", onDragHandler);
109-
dom.removeEventListener("scroll", onScrollHandler);
11072
};
11173
},
11274
} as const;

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export * from "./editor/defaultColors.js";
1414
export * from "./editor/selectionTypes.js";
1515
export * from "./exporter/index.js";
1616
export * from "./extensions-shared/UiElementPosition.js";
17+
export * from "./extensions/Comments/CommentsPlugin.js";
1718
export * from "./extensions/FilePanel/FilePanelPlugin.js";
1819
export * from "./extensions/FormattingToolbar/FormattingToolbarPlugin.js";
1920
export * from "./extensions/LinkToolbar/LinkToolbar.js";
Lines changed: 24 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,42 @@
1-
import {
2-
BlockSchema,
3-
DefaultBlockSchema,
4-
DefaultInlineContentSchema,
5-
DefaultStyleSchema,
6-
FilePanelPlugin,
7-
InlineContentSchema,
8-
StyleSchema,
9-
} from "@blocknote/core";
1+
import { FilePanelPlugin } from "@blocknote/core";
102
import { UseFloatingOptions, flip, offset } from "@floating-ui/react";
11-
import { FC } from "react";
3+
import { FC, useCallback, useMemo } from "react";
124

13-
import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
14-
import { useUIElementPositioning } from "../../hooks/useUIElementPositioning.js";
155
import { FilePanel } from "./FilePanel.js";
166
import { FilePanelProps } from "./FilePanelProps.js";
17-
import { usePluginState } from "../../hooks/usePlugin.js";
7+
import { BlockPopover } from "../Popovers/BlockPopover.js";
8+
import { usePlugin, usePluginState } from "../../hooks/usePlugin.js";
189

19-
export const FilePanelController = <
20-
B extends BlockSchema = DefaultBlockSchema,
21-
I extends InlineContentSchema = DefaultInlineContentSchema,
22-
S extends StyleSchema = DefaultStyleSchema,
23-
>(props: {
10+
export const FilePanelController = (props: {
2411
filePanel?: FC<FilePanelProps>;
25-
floatingOptions?: Partial<UseFloatingOptions>;
12+
floatingUIOptions?: UseFloatingOptions;
2613
}) => {
27-
const editor = useBlockNoteEditor<B, I, S>();
28-
29-
if (!editor.filePanel) {
30-
throw new Error(
31-
"FileToolbarController can only be used when BlockNote editor schema contains file block",
32-
);
33-
}
14+
const filePanel = usePlugin(FilePanelPlugin);
15+
const state = usePluginState(FilePanelPlugin, {
16+
selector: (state) => {
17+
return {
18+
blockId: state.blockId || filePanel.store.prevState.blockId,
19+
show: state.blockId !== undefined,
20+
};
21+
},
22+
});
3423

35-
// TODO refactor this to use a hook for positioning to a block
36-
const state = usePluginState(FilePanelPlugin);
24+
const getBlockId = useCallback(() => state.blockId, [state.blockId]);
3725

38-
const { isMounted, ref, style, getFloatingProps } = useUIElementPositioning(
39-
!!state?.blockId,
40-
state?.referencePos || null,
41-
5000,
42-
{
43-
placement: "bottom",
26+
const floatingUIOptions = useMemo<UseFloatingOptions>(
27+
() => ({
28+
open: state.show,
4429
middleware: [offset(10), flip()],
45-
onOpenChange: (open) => {
46-
if (!open) {
47-
editor.filePanel!.closeMenu();
48-
editor.focus();
49-
}
50-
},
51-
...props.floatingOptions,
52-
},
30+
...props.floatingUIOptions,
31+
}),
32+
[props.floatingUIOptions, state.show],
5333
);
5434

55-
if (!isMounted || !state) {
56-
return null;
57-
}
58-
5935
const Component = props.filePanel || FilePanel;
6036

6137
return (
62-
<div ref={ref} style={style} {...getFloatingProps()}>
38+
<BlockPopover getBlockId={getBlockId} floatingUIOptions={floatingUIOptions}>
6339
{state.blockId && <Component blockId={state.blockId} />}
64-
</div>
40+
</BlockPopover>
6541
);
6642
};

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ export const getFormattingToolbarItems = (
2929
blockTypeSelectItems?: BlockTypeSelectItem[],
3030
): JSX.Element[] => [
3131
<BlockTypeSelect key={"blockTypeSelect"} items={blockTypeSelectItems} />,
32-
<TableCellMergeButton key={"tableCellMergeButton"} />,
33-
<FileCaptionButton key={"fileCaptionButton"} />,
34-
<FileReplaceButton key={"replaceFileButton"} />,
35-
<FileRenameButton key={"fileRenameButton"} />,
32+
// <TableCellMergeButton key={"tableCellMergeButton"} />,
33+
// <FileCaptionButton key={"fileCaptionButton"} />,
34+
// <FileReplaceButton key={"replaceFileButton"} />,
35+
// <FileRenameButton key={"fileRenameButton"} />,
3636
<FileDeleteButton key={"fileDeleteButton"} />,
3737
<FileDownloadButton key={"fileDownloadButton"} />,
3838
<FilePreviewButton key={"filePreviewButton"} />,
@@ -49,7 +49,7 @@ export const getFormattingToolbarItems = (
4949
<ColorStyleButton key={"colorStyleButton"} />,
5050
<NestBlockButton key={"nestBlockButton"} />,
5151
<UnnestBlockButton key={"unnestBlockButton"} />,
52-
<CreateLinkButton key={"createLinkButton"} />,
52+
// <CreateLinkButton key={"createLinkButton"} />,
5353
<AddCommentButton key={"addCommentButton"} />,
5454
<AddTiptapCommentButton key={"addTiptapCommentButton"} />,
5555
];
Lines changed: 37 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import {
22
blockHasType,
3+
BlockNoteEditor,
34
BlockSchema,
45
defaultProps,
56
DefaultProps,
7+
FormattingToolbarExtension,
68
InlineContentSchema,
79
StyleSchema,
810
} from "@blocknote/core";
911
import { UseFloatingOptions, flip, offset, shift } from "@floating-ui/react";
10-
import { isEventTargetWithin } from "@floating-ui/react/utils";
11-
import { FC, useMemo, useRef } from "react";
12+
import { FC, useCallback, useMemo } from "react";
1213

1314
import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
1415
import { useEditorState } from "../../hooks/useEditorState.js";
15-
import { useUIElementPositioning } from "../../hooks/useUIElementPositioning.js";
16-
import { useUIPluginState } from "../../hooks/useUIPluginState.js";
17-
import { mergeRefs } from "../../util/mergeRefs.js";
16+
import { usePluginState } from "../../hooks/usePlugin.js";
17+
import { PositionPopover } from "../Popovers/PositionPopover.js";
1818
import { FormattingToolbar } from "./FormattingToolbar.js";
1919
import { FormattingToolbarProps } from "./FormattingToolbarProps.js";
2020

@@ -35,9 +35,19 @@ const textAlignmentToPlacement = (
3535

3636
export const FormattingToolbarController = (props: {
3737
formattingToolbar?: FC<FormattingToolbarProps>;
38-
floatingOptions?: Partial<UseFloatingOptions>;
38+
floatingUIOptions?: UseFloatingOptions;
3939
}) => {
40-
const divRef = useRef<HTMLDivElement>(null);
40+
const show = usePluginState(FormattingToolbarExtension, {
41+
selector: (state) => state.show,
42+
});
43+
44+
const getPosition = useCallback(
45+
(editor: BlockNoteEditor<any, any, any>) => ({
46+
from: editor.prosemirrorState.selection.from,
47+
to: editor.prosemirrorState.selection.to,
48+
}),
49+
[],
50+
);
4151

4252
const editor = useBlockNoteEditor<
4353
BlockSchema,
@@ -62,73 +72,24 @@ export const FormattingToolbarController = (props: {
6272
},
6373
});
6474

65-
// TODO refactor this to actually use the new extension & a hook for positioning to a selection
66-
67-
return null;
68-
69-
// const state = useUIPluginState(
70-
// editor.formattingToolbar.onUpdate.bind(editor.formattingToolbar),
71-
// );
72-
73-
// const { isMounted, ref, style, getFloatingProps } = useUIElementPositioning(
74-
// state?.show || false,
75-
// state?.referencePos || null,
76-
// 3000,
77-
// {
78-
// placement,
79-
// middleware: [offset(10), shift(), flip()],
80-
// onOpenChange: (open, _event) => {
81-
// // console.log("change", event);
82-
// if (!open) {
83-
// editor.formattingToolbar.closeMenu();
84-
// editor.focus();
85-
// }
86-
// },
87-
// canDismiss: {
88-
// enabled: true,
89-
// escapeKey: true,
90-
// outsidePress: (e) => {
91-
// const view = editor._tiptapEditor?.view;
92-
// if (!view) {
93-
// return false;
94-
// }
95-
96-
// const target = e.target;
97-
// if (!target) {
98-
// return false;
99-
// }
100-
101-
// return !isEventTargetWithin(e, view.dom.parentElement);
102-
// },
103-
// },
104-
// ...props.floatingOptions,
105-
// },
106-
// );
107-
108-
// const combinedRef = useMemo(() => mergeRefs([divRef, ref]), [divRef, ref]);
109-
110-
// if (!isMounted || !state) {
111-
// return null;
112-
// }
113-
114-
// if (!state.show && divRef.current) {
115-
// // The component is fading out. Use the previous state to render the toolbar with innerHTML,
116-
// // because otherwise the toolbar will quickly flickr (i.e.: show a different state) while fading out,
117-
// // which looks weird
118-
// return (
119-
// <div
120-
// ref={combinedRef}
121-
// style={style}
122-
// dangerouslySetInnerHTML={{ __html: divRef.current.innerHTML }}
123-
// ></div>
124-
// );
125-
// }
126-
127-
// const Component = props.formattingToolbar || FormattingToolbar;
128-
129-
// return (
130-
// <div ref={combinedRef} style={style} {...getFloatingProps()}>
131-
// <Component />
132-
// </div>
133-
// );
75+
const floatingUIOptions = useMemo<UseFloatingOptions>(
76+
() => ({
77+
open: show,
78+
placement,
79+
middleware: [offset(10), shift(), flip()],
80+
...props.floatingUIOptions,
81+
}),
82+
[placement, props.floatingUIOptions, show],
83+
);
84+
85+
const Component = props.formattingToolbar || FormattingToolbar;
86+
87+
return (
88+
<PositionPopover
89+
getPosition={getPosition}
90+
floatingUIOptions={floatingUIOptions}
91+
>
92+
<Component />
93+
</PositionPopover>
94+
);
13495
};

0 commit comments

Comments
 (0)