@blocknote/core 0.1.0-alpha.3 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/README.md +4 -2
- package/dist/blocknote.js +3461 -2429
- package/dist/blocknote.js.map +1 -1
- package/dist/blocknote.umd.cjs +35 -71
- package/dist/blocknote.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +9 -7
- package/src/BlockNoteExtensions.ts +10 -17
- package/src/BlockNoteTheme.ts +150 -0
- package/src/EditorContent.tsx +2 -1
- package/src/extensions/Blocks/BlockAttributes.ts +12 -0
- package/src/extensions/Blocks/MultipleNodeSelection.ts +87 -0
- package/src/extensions/Blocks/OrderedListPlugin.ts +2 -2
- package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +8 -2
- package/src/extensions/Blocks/helpers/findBlock.ts +1 -1
- package/src/extensions/Blocks/nodes/Block.module.css +37 -37
- package/src/extensions/Blocks/nodes/Block.ts +89 -45
- package/src/extensions/Blocks/nodes/BlockGroup.ts +19 -2
- package/src/extensions/Blocks/nodes/Content.ts +15 -2
- package/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +10 -2
- package/src/extensions/BubbleMenu/component/BubbleMenu.tsx +122 -98
- package/src/extensions/BubbleMenu/component/LinkToolbarButton.tsx +8 -8
- package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx +143 -33
- package/src/extensions/DraggableBlocks/components/DragHandle.tsx +15 -21
- package/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx +8 -7
- package/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +31 -66
- package/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.tsx +44 -0
- package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItem.tsx +34 -0
- package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemIcon.tsx +31 -0
- package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemInput.tsx +40 -0
- package/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.tsx +37 -0
- package/src/extensions/Hyperlinks/menus/HyperlinkMenu.tsx +63 -0
- package/src/extensions/SlashMenu/SlashMenuItem.ts +3 -1
- package/src/extensions/SlashMenu/defaultCommands.tsx +4 -4
- package/src/extensions/TrailingNode/TrailingNodeExtension.ts +8 -5
- package/src/shared/components/toolbar/Toolbar.tsx +8 -3
- package/src/shared/components/toolbar/ToolbarButton.tsx +57 -0
- package/src/shared/components/toolbar/ToolbarDropdown.tsx +35 -0
- package/src/shared/components/toolbar/ToolbarDropdownItem.tsx +35 -0
- package/src/shared/components/toolbar/ToolbarDropdownTarget.tsx +31 -0
- package/src/shared/plugins/suggestion/SuggestionItem.ts +3 -1
- package/src/shared/plugins/suggestion/{SuggestionListReactRenderer.ts → SuggestionListReactRenderer.tsx} +13 -4
- package/src/shared/plugins/suggestion/components/SuggestionGroup.tsx +6 -93
- package/src/shared/plugins/suggestion/components/SuggestionGroupItem.tsx +82 -0
- package/src/shared/plugins/suggestion/components/SuggestionList.tsx +24 -23
- package/src/useEditor.ts +4 -0
- package/src/utils.ts +12 -0
- package/types/src/BlockNoteExtensions.d.ts +3 -0
- package/types/src/BlockNoteTheme.d.ts +2 -0
- package/types/src/commands/indentation.d.ts +2 -0
- package/types/src/extensions/Blocks/BlockAttributes.d.ts +2 -0
- package/types/src/extensions/Blocks/MultipleNodeSelection.d.ts +24 -0
- package/types/src/extensions/Blocks/nodes/Block.d.ts +1 -1
- package/types/src/extensions/BubbleMenu/component/LinkToolbarButton.d.ts +2 -2
- package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.d.ts +11 -0
- package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItem.d.ts +13 -0
- package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemIcon.d.ts +8 -0
- package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemInput.d.ts +9 -0
- package/types/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.d.ts +12 -0
- package/types/src/extensions/Hyperlinks/menus/HyperlinkMenu.d.ts +21 -0
- package/types/src/extensions/Hyperlinks/menus/helpers/PanelTextInput.d.ts +39 -0
- package/types/src/extensions/Hyperlinks/menus/helpers/PanelTextInputStyles.d.ts +3 -0
- package/types/src/extensions/Hyperlinks/menus/helpers/ToolbarComponent.d.ts +13 -0
- package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +4 -7
- package/types/src/extensions/TrailingNode/TrailingNodeExtension.d.ts +3 -0
- package/types/src/nodes/ChildgroupNode.d.ts +28 -0
- package/types/src/nodes/patchNodes.d.ts +1 -0
- package/types/src/plugins/TreeViewPlugin/index.d.ts +2 -0
- package/types/src/plugins/animation.d.ts +2 -0
- package/types/src/react/BlockNoteComposer.d.ts +17 -0
- package/types/src/react/BlockNotePlugin.d.ts +1 -0
- package/types/src/react/index.d.ts +3 -0
- package/types/src/react/useBlockNoteSetup.d.ts +2 -0
- package/types/src/registerBlockNote.d.ts +2 -0
- package/types/src/shared/components/toolbar/SimpleToolbarButton.d.ts +2 -3
- package/types/src/shared/components/toolbar/SimpleToolbarDropdown.d.ts +11 -0
- package/types/src/shared/components/toolbar/SimpleToolbarDropdownItem.d.ts +11 -0
- package/types/src/shared/components/toolbar/Toolbar.d.ts +2 -2
- package/types/src/shared/components/toolbar/ToolbarButton.d.ts +15 -0
- package/types/src/shared/components/toolbar/ToolbarDropdown.d.ts +17 -0
- package/types/src/shared/components/toolbar/ToolbarDropdownItem.d.ts +11 -0
- package/types/src/shared/components/toolbar/ToolbarDropdownTarget.d.ts +8 -0
- package/types/src/shared/plugins/suggestion/SuggestionItem.d.ts +2 -4
- package/types/src/shared/plugins/suggestion/components/SuggestionGroupItem.d.ts +9 -0
- package/types/src/shared/plugins/suggestion/components/SuggestionList.d.ts +0 -15
- package/types/src/themes/BlockNoteEditorTheme.d.ts +11 -0
- package/types/src/useEditor.d.ts +3 -0
- package/types/src/utils.d.ts +2 -0
- package/src/extensions/Blocks/nodes/README.md +0 -26
- package/src/extensions/BubbleMenu/component/DropdownBlockItem.module.css +0 -13
- package/src/extensions/BubbleMenu/component/DropdownBlockItem.tsx +0 -25
- package/src/extensions/DraggableBlocks/components/DragHandle.module.css +0 -33
- package/src/extensions/DraggableBlocks/components/DragHandleMenu.module.css +0 -10
- package/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.tsx +0 -59
- package/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.tsx +0 -72
- package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.tsx +0 -173
- package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.ts +0 -36
- package/src/extensions/Hyperlinks/menus/atlaskit/README.md +0 -1
- package/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.tsx +0 -61
- package/src/extensions/helpers/formatKeyboardShortcut.ts +0 -9
- package/src/lib/atlaskit/browser.ts +0 -47
- package/src/shared/components/toolbar/SimpleToolbarButton.module.css +0 -13
- package/src/shared/components/toolbar/SimpleToolbarButton.tsx +0 -56
- package/src/shared/components/toolbar/Toolbar.module.css +0 -10
- package/src/shared/components/toolbar/ToolbarSeparator.module.css +0 -13
- package/src/shared/components/toolbar/ToolbarSeparator.tsx +0 -7
- package/src/shared/plugins/suggestion/components/SuggestionGroup.module.css +0 -45
- package/src/shared/plugins/suggestion/components/SuggestionList.module.css +0 -10
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import Tippy from "@tippyjs/react";
|
|
3
|
+
import { EditHyperlinkMenu } from "./EditHyperlinkMenu";
|
|
4
|
+
import { HoverHyperlinkMenu } from "./HoverHyperlinkMenu";
|
|
5
|
+
import rootStyles from "../../../root.module.css";
|
|
6
|
+
|
|
7
|
+
type HyperlinkMenuProps = {
|
|
8
|
+
url: string;
|
|
9
|
+
text: string;
|
|
10
|
+
pos: {
|
|
11
|
+
height: number;
|
|
12
|
+
width: number;
|
|
13
|
+
left: number;
|
|
14
|
+
right: number;
|
|
15
|
+
top: number;
|
|
16
|
+
bottom: number;
|
|
17
|
+
};
|
|
18
|
+
update: (url: string, text: string) => void;
|
|
19
|
+
remove: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Main menu component for the hyperlink extension.
|
|
24
|
+
* Either renders a menu to create/edit a hyperlink, or a menu to interact with it on mouse hover.
|
|
25
|
+
*/
|
|
26
|
+
export const HyperlinkMenu = (props: HyperlinkMenuProps) => {
|
|
27
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
28
|
+
|
|
29
|
+
const editHyperlinkMenu = (
|
|
30
|
+
<Tippy
|
|
31
|
+
getReferenceClientRect={() => props.pos as any}
|
|
32
|
+
content={
|
|
33
|
+
<EditHyperlinkMenu
|
|
34
|
+
url={props.url}
|
|
35
|
+
text={props.text}
|
|
36
|
+
update={props.update}
|
|
37
|
+
/>
|
|
38
|
+
}
|
|
39
|
+
interactive={true}
|
|
40
|
+
interactiveBorder={30}
|
|
41
|
+
showOnCreate={true}
|
|
42
|
+
trigger={"click"} // so that we don't hide on mouse out
|
|
43
|
+
hideOnClick
|
|
44
|
+
className={rootStyles.bnRoot}
|
|
45
|
+
appendTo={document.body}>
|
|
46
|
+
<div></div>
|
|
47
|
+
</Tippy>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const hoverHyperlinkMenu = (
|
|
51
|
+
<HoverHyperlinkMenu
|
|
52
|
+
url={props.url}
|
|
53
|
+
edit={() => setIsEditing(true)}
|
|
54
|
+
remove={props.remove}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (isEditing) {
|
|
59
|
+
return editHyperlinkMenu;
|
|
60
|
+
} else {
|
|
61
|
+
return hoverHyperlinkMenu;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Editor, Range } from "@tiptap/core";
|
|
2
2
|
import SuggestionItem from "../../shared/plugins/suggestion/SuggestionItem";
|
|
3
|
+
import { IconType } from "react-icons";
|
|
3
4
|
|
|
4
5
|
export type SlashMenuCallback = (editor: Editor, range: Range) => boolean;
|
|
5
6
|
|
|
@@ -31,6 +32,7 @@ export class SlashMenuItem implements SuggestionItem {
|
|
|
31
32
|
* @param execute The callback for creating a new node
|
|
32
33
|
* @param aliases Aliases for this command
|
|
33
34
|
* @param icon To be shown next to the name in the menu
|
|
35
|
+
* @param hint Short description of command
|
|
34
36
|
* @param shortcut Info about keyboard shortcut that would activate this command
|
|
35
37
|
*/
|
|
36
38
|
constructor(
|
|
@@ -38,7 +40,7 @@ export class SlashMenuItem implements SuggestionItem {
|
|
|
38
40
|
public readonly group: SlashMenuGroups,
|
|
39
41
|
public readonly execute: SlashMenuCallback,
|
|
40
42
|
public readonly aliases: string[] = [],
|
|
41
|
-
public readonly icon?:
|
|
43
|
+
public readonly icon?: IconType,
|
|
42
44
|
public readonly hint?: string,
|
|
43
45
|
public readonly shortcut?: string
|
|
44
46
|
) {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
RiListUnordered,
|
|
7
7
|
RiText,
|
|
8
8
|
} from "react-icons/ri";
|
|
9
|
-
import formatKeyboardShortcut from "
|
|
9
|
+
import { formatKeyboardShortcut } from "../../utils";
|
|
10
10
|
import { SlashMenuGroups, SlashMenuItem } from "./SlashMenuItem";
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -22,7 +22,7 @@ const defaultCommands: { [key: string]: SlashMenuItem } = {
|
|
|
22
22
|
.chain()
|
|
23
23
|
.focus()
|
|
24
24
|
.deleteRange(range)
|
|
25
|
-
.addNewBlockAsSibling({ headingType: 1 })
|
|
25
|
+
.addNewBlockAsSibling({ headingType: "1" })
|
|
26
26
|
.run();
|
|
27
27
|
},
|
|
28
28
|
["h", "heading1", "h1"],
|
|
@@ -40,7 +40,7 @@ const defaultCommands: { [key: string]: SlashMenuItem } = {
|
|
|
40
40
|
.chain()
|
|
41
41
|
.focus()
|
|
42
42
|
.deleteRange(range)
|
|
43
|
-
.addNewBlockAsSibling({ headingType: 2 })
|
|
43
|
+
.addNewBlockAsSibling({ headingType: "2" })
|
|
44
44
|
.run();
|
|
45
45
|
},
|
|
46
46
|
["h2", "heading2", "subheading"],
|
|
@@ -58,7 +58,7 @@ const defaultCommands: { [key: string]: SlashMenuItem } = {
|
|
|
58
58
|
.chain()
|
|
59
59
|
.focus()
|
|
60
60
|
.deleteRange(range)
|
|
61
|
-
.addNewBlockAsSibling({ headingType: 3 })
|
|
61
|
+
.addNewBlockAsSibling({ headingType: "3" })
|
|
62
62
|
.run();
|
|
63
63
|
},
|
|
64
64
|
["h3", "heading3", "subheading"],
|
|
@@ -13,6 +13,9 @@ export interface TrailingNodeOptions {
|
|
|
13
13
|
node: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Add a trailing node to the document so the user can always click at the bottom of the document and start typing
|
|
18
|
+
*/
|
|
16
19
|
export const TrailingNode = Extension.create<TrailingNodeOptions>({
|
|
17
20
|
name: "trailingNode",
|
|
18
21
|
|
|
@@ -29,8 +32,8 @@ export const TrailingNode = Extension.create<TrailingNodeOptions>({
|
|
|
29
32
|
const { doc, tr, schema } = state;
|
|
30
33
|
const shouldInsertNodeAtEnd = plugin.getState(state);
|
|
31
34
|
const endPosition = doc.content.size - 2;
|
|
32
|
-
const type = schema.nodes["
|
|
33
|
-
const contenttype = schema.nodes["
|
|
35
|
+
const type = schema.nodes["block"];
|
|
36
|
+
const contenttype = schema.nodes["content"];
|
|
34
37
|
if (!shouldInsertNodeAtEnd) {
|
|
35
38
|
return;
|
|
36
39
|
}
|
|
@@ -58,10 +61,10 @@ export const TrailingNode = Extension.create<TrailingNodeOptions>({
|
|
|
58
61
|
|
|
59
62
|
lastNode = lastNode.lastChild;
|
|
60
63
|
|
|
61
|
-
if (!lastNode || lastNode.type.name !== "
|
|
62
|
-
throw new Error("Expected
|
|
64
|
+
if (!lastNode || lastNode.type.name !== "block") {
|
|
65
|
+
throw new Error("Expected block");
|
|
63
66
|
}
|
|
64
|
-
return lastNode.nodeSize > 4; // empty <
|
|
67
|
+
return lastNode.nodeSize > 4; // empty <block><content/></block> is length 4
|
|
65
68
|
},
|
|
66
69
|
},
|
|
67
70
|
}),
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createStyles, Group } from "@mantine/core";
|
|
2
|
+
import { ReactNode } from "react";
|
|
2
3
|
|
|
3
|
-
export const Toolbar = (props: { children:
|
|
4
|
-
|
|
4
|
+
export const Toolbar = (props: { children: ReactNode }) => {
|
|
5
|
+
const { classes } = createStyles({ root: {} })(undefined, {
|
|
6
|
+
name: "Toolbar",
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
return <Group className={classes.root}>{props.children}</Group>;
|
|
5
10
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ActionIcon, Button } from "@mantine/core";
|
|
2
|
+
import Tippy from "@tippyjs/react";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import { TooltipContent } from "../tooltip/TooltipContent";
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { IconType } from "react-icons";
|
|
7
|
+
|
|
8
|
+
export type ToolbarButtonProps = {
|
|
9
|
+
onClick?: (e: React.MouseEvent) => void;
|
|
10
|
+
icon?: IconType;
|
|
11
|
+
mainTooltip: string;
|
|
12
|
+
secondaryTooltip?: string;
|
|
13
|
+
isSelected?: boolean;
|
|
14
|
+
children?: any;
|
|
15
|
+
isDisabled?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Helper for basic buttons that show in the inline bubble menu.
|
|
20
|
+
*/
|
|
21
|
+
export const ToolbarButton = forwardRef((props: ToolbarButtonProps, ref) => {
|
|
22
|
+
const ButtonIcon = props.icon;
|
|
23
|
+
return (
|
|
24
|
+
<Tippy
|
|
25
|
+
content={
|
|
26
|
+
<TooltipContent
|
|
27
|
+
mainTooltip={props.mainTooltip}
|
|
28
|
+
secondaryTooltip={props.secondaryTooltip}
|
|
29
|
+
/>
|
|
30
|
+
}
|
|
31
|
+
trigger={"mouseenter"}>
|
|
32
|
+
{/*Creates an ActionIcon instead of a Button if only an icon is provided as content.*/}
|
|
33
|
+
{props.children ? (
|
|
34
|
+
<Button
|
|
35
|
+
onClick={props.onClick}
|
|
36
|
+
color={"brandFinal"}
|
|
37
|
+
size={"xs"}
|
|
38
|
+
variant={props.isSelected ? "filled" : "subtle"}
|
|
39
|
+
disabled={props.isDisabled || false}
|
|
40
|
+
ref={ref as any}>
|
|
41
|
+
{ButtonIcon && <ButtonIcon />}
|
|
42
|
+
{props.children}
|
|
43
|
+
</Button>
|
|
44
|
+
) : (
|
|
45
|
+
<ActionIcon
|
|
46
|
+
onClick={props.onClick}
|
|
47
|
+
color={"brandFinal"}
|
|
48
|
+
size={30}
|
|
49
|
+
variant={props.isSelected ? "filled" : "subtle"}
|
|
50
|
+
disabled={props.isDisabled || false}
|
|
51
|
+
ref={ref as any}>
|
|
52
|
+
{ButtonIcon && <ButtonIcon />}
|
|
53
|
+
</ActionIcon>
|
|
54
|
+
)}
|
|
55
|
+
</Tippy>
|
|
56
|
+
);
|
|
57
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Menu } from "@mantine/core";
|
|
2
|
+
import { IconType } from "react-icons";
|
|
3
|
+
import { ToolbarDropdownTarget } from "./ToolbarDropdownTarget";
|
|
4
|
+
import { MouseEvent } from "react";
|
|
5
|
+
import { ToolbarDropdownItem } from "./ToolbarDropdownItem";
|
|
6
|
+
|
|
7
|
+
export type ToolbarDropdownProps = {
|
|
8
|
+
text: string;
|
|
9
|
+
icon?: IconType;
|
|
10
|
+
items: Array<{
|
|
11
|
+
onClick?: (e: MouseEvent) => void;
|
|
12
|
+
text: string;
|
|
13
|
+
icon?: IconType;
|
|
14
|
+
isSelected?: boolean;
|
|
15
|
+
children?: any;
|
|
16
|
+
isDisabled?: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
children?: any;
|
|
19
|
+
isDisabled?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function ToolbarDropdown(props: ToolbarDropdownProps) {
|
|
23
|
+
return (
|
|
24
|
+
<Menu exitTransitionDuration={0}>
|
|
25
|
+
<Menu.Target>
|
|
26
|
+
<ToolbarDropdownTarget {...props} />
|
|
27
|
+
</Menu.Target>
|
|
28
|
+
<Menu.Dropdown>
|
|
29
|
+
{props.items.map((item) => (
|
|
30
|
+
<ToolbarDropdownItem key={item.text} {...item} />
|
|
31
|
+
))}
|
|
32
|
+
</Menu.Dropdown>
|
|
33
|
+
</Menu>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Menu } from "@mantine/core";
|
|
2
|
+
import { IconType } from "react-icons";
|
|
3
|
+
import { TiTick } from "react-icons/ti";
|
|
4
|
+
import { MouseEvent } from "react";
|
|
5
|
+
|
|
6
|
+
export type ToolbarDropdownItemProps = {
|
|
7
|
+
onClick?: (e: MouseEvent) => void;
|
|
8
|
+
text: string;
|
|
9
|
+
icon?: IconType;
|
|
10
|
+
isSelected?: boolean;
|
|
11
|
+
children?: any;
|
|
12
|
+
isDisabled?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function ToolbarDropdownItem(props: ToolbarDropdownItemProps) {
|
|
16
|
+
const ItemIcon = props.icon;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Menu.Item
|
|
20
|
+
key={props.text}
|
|
21
|
+
onClick={props.onClick}
|
|
22
|
+
icon={ItemIcon && <ItemIcon size={16} />}
|
|
23
|
+
rightSection={
|
|
24
|
+
props.isSelected ? (
|
|
25
|
+
<TiTick size={16} />
|
|
26
|
+
) : (
|
|
27
|
+
// Ensures space for tick even if item isn't currently selected.
|
|
28
|
+
<div style={{ width: "16px", padding: "0" }} />
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
disabled={props.isDisabled}>
|
|
32
|
+
{props.text}
|
|
33
|
+
</Menu.Item>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Button } from "@mantine/core";
|
|
2
|
+
import { HiChevronDown } from "react-icons/hi";
|
|
3
|
+
import { IconType } from "react-icons";
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
|
|
6
|
+
export type ToolbarDropdownTargetProps = {
|
|
7
|
+
text: string;
|
|
8
|
+
icon?: IconType;
|
|
9
|
+
isDisabled?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const ToolbarDropdownTarget = forwardRef<
|
|
13
|
+
HTMLButtonElement,
|
|
14
|
+
ToolbarDropdownTargetProps
|
|
15
|
+
>((props: ToolbarDropdownTargetProps, ref) => {
|
|
16
|
+
const { text, icon, isDisabled, ...others } = props;
|
|
17
|
+
|
|
18
|
+
const TargetIcon = props.icon;
|
|
19
|
+
return (
|
|
20
|
+
<Button
|
|
21
|
+
leftIcon={TargetIcon && <TargetIcon size={16} />}
|
|
22
|
+
rightIcon={<HiChevronDown />}
|
|
23
|
+
size={"xs"}
|
|
24
|
+
variant={"subtle"}
|
|
25
|
+
disabled={props.isDisabled}
|
|
26
|
+
ref={ref}
|
|
27
|
+
{...others}>
|
|
28
|
+
{props.text}
|
|
29
|
+
</Button>
|
|
30
|
+
);
|
|
31
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { IconType } from "react-icons";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* A generic interface used in all suggestion menus (slash menu, mentions, etc)
|
|
3
5
|
*/
|
|
@@ -15,7 +17,7 @@ export default interface SuggestionItem {
|
|
|
15
17
|
/**
|
|
16
18
|
* The react icon
|
|
17
19
|
*/
|
|
18
|
-
icon?:
|
|
20
|
+
icon?: IconType;
|
|
19
21
|
|
|
20
22
|
hint?: string;
|
|
21
23
|
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
SuggestionList,
|
|
7
7
|
SuggestionListProps,
|
|
8
8
|
} from "./components/SuggestionList";
|
|
9
|
+
import { BlockNoteTheme } from "../../../BlockNoteTheme";
|
|
10
|
+
import { MantineProvider } from "@mantine/core";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* The interface that each suggestion renderer should conform to.
|
|
@@ -140,10 +142,17 @@ export default function createRenderer<T extends SuggestionItem>(
|
|
|
140
142
|
selectedIndex,
|
|
141
143
|
};
|
|
142
144
|
|
|
143
|
-
component = new ReactRenderer(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
component = new ReactRenderer(
|
|
146
|
+
(props: SuggestionListProps<T>) => (
|
|
147
|
+
<MantineProvider theme={BlockNoteTheme}>
|
|
148
|
+
<SuggestionList {...props} />
|
|
149
|
+
</MantineProvider>
|
|
150
|
+
),
|
|
151
|
+
{
|
|
152
|
+
editor: editor as ReactEditor,
|
|
153
|
+
props: componentProps,
|
|
154
|
+
}
|
|
155
|
+
);
|
|
147
156
|
|
|
148
157
|
popup = tippy("body", {
|
|
149
158
|
getReferenceClientRect: newProps.clientRect,
|
|
@@ -1,94 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import React from "react";
|
|
1
|
+
import { Menu } from "@mantine/core";
|
|
3
2
|
import SuggestionItem from "../SuggestionItem";
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
const MIN_LEFT_MARGIN = 5;
|
|
7
|
-
|
|
8
|
-
function SuggestionContent<T extends SuggestionItem>(props: { item: T }) {
|
|
9
|
-
return (
|
|
10
|
-
<div className={styles.suggestionWrapper}>
|
|
11
|
-
<div>
|
|
12
|
-
<div className={styles.buttonName}>{props.item.name}</div>
|
|
13
|
-
{props.item.hint && (
|
|
14
|
-
<div className={styles.buttonHint}>{props.item.hint}</div>
|
|
15
|
-
)}
|
|
16
|
-
</div>
|
|
17
|
-
{props.item.shortcut && (
|
|
18
|
-
<div>
|
|
19
|
-
<div className={styles.buttonShortcut}>{props.item.shortcut}</div>
|
|
20
|
-
</div>
|
|
21
|
-
)}
|
|
22
|
-
</div>
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function getIcon<T extends SuggestionItem>(
|
|
27
|
-
item: T,
|
|
28
|
-
isButtonSelected: boolean
|
|
29
|
-
): JSX.Element | undefined {
|
|
30
|
-
const Icon = item.icon;
|
|
31
|
-
return (
|
|
32
|
-
Icon && (
|
|
33
|
-
<div className={styles.iconWrapper}>
|
|
34
|
-
<Icon
|
|
35
|
-
className={
|
|
36
|
-
styles.icon + " " + (isButtonSelected ? styles.selectedIcon : "")
|
|
37
|
-
}
|
|
38
|
-
/>
|
|
39
|
-
</div>
|
|
40
|
-
)
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
type SuggestionComponentProps<T> = {
|
|
45
|
-
item: T;
|
|
46
|
-
index: number;
|
|
47
|
-
selectedIndex?: number;
|
|
48
|
-
clickItem: (item: T) => void;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
function SuggestionComponent<T extends SuggestionItem>(
|
|
52
|
-
props: SuggestionComponentProps<T>
|
|
53
|
-
) {
|
|
54
|
-
let isButtonSelected =
|
|
55
|
-
props.selectedIndex !== undefined && props.selectedIndex === props.index;
|
|
56
|
-
|
|
57
|
-
const buttonRef = React.useRef<HTMLElement>(null);
|
|
58
|
-
React.useEffect(() => {
|
|
59
|
-
if (
|
|
60
|
-
isButtonSelected &&
|
|
61
|
-
buttonRef.current &&
|
|
62
|
-
buttonRef.current.getBoundingClientRect().left > MIN_LEFT_MARGIN //TODO: Kinda hacky, fix
|
|
63
|
-
// This check is needed because initially the menu is initialized somewhere above outside the screen (with left = 1)
|
|
64
|
-
// scrollIntoView() is called before the menu is set in the right place, and without the check would scroll to the top of the page every time
|
|
65
|
-
) {
|
|
66
|
-
buttonRef.current.scrollIntoView({
|
|
67
|
-
behavior: "smooth",
|
|
68
|
-
block: "nearest",
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
}, [isButtonSelected]);
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<div className={styles.buttonItem}>
|
|
75
|
-
<ButtonItem
|
|
76
|
-
isSelected={isButtonSelected} // This is needed to navigate with the keyboard
|
|
77
|
-
iconBefore={getIcon(props.item, isButtonSelected)}
|
|
78
|
-
onClick={(_e) => {
|
|
79
|
-
setTimeout(() => {
|
|
80
|
-
props.clickItem(props.item);
|
|
81
|
-
}, 0);
|
|
82
|
-
|
|
83
|
-
// e.stopPropagation();
|
|
84
|
-
// e.preventDefault();
|
|
85
|
-
}}
|
|
86
|
-
ref={buttonRef}>
|
|
87
|
-
<SuggestionContent item={props.item} />
|
|
88
|
-
</ButtonItem>
|
|
89
|
-
</div>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
3
|
+
import { SuggestionGroupItem } from "./SuggestionGroupItem";
|
|
92
4
|
|
|
93
5
|
type SuggestionGroupProps<T> = {
|
|
94
6
|
/**
|
|
@@ -117,10 +29,11 @@ export function SuggestionGroup<T extends SuggestionItem>(
|
|
|
117
29
|
props: SuggestionGroupProps<T>
|
|
118
30
|
) {
|
|
119
31
|
return (
|
|
120
|
-
|
|
32
|
+
<>
|
|
33
|
+
<Menu.Label>{props.name}</Menu.Label>
|
|
121
34
|
{props.items.map((item, index) => {
|
|
122
35
|
return (
|
|
123
|
-
<
|
|
36
|
+
<SuggestionGroupItem
|
|
124
37
|
item={item}
|
|
125
38
|
key={index} // TODO: using index as key is not ideal for performance, better have ids on suggestionItems
|
|
126
39
|
index={index}
|
|
@@ -129,6 +42,6 @@ export function SuggestionGroup<T extends SuggestionItem>(
|
|
|
129
42
|
/>
|
|
130
43
|
);
|
|
131
44
|
})}
|
|
132
|
-
|
|
45
|
+
</>
|
|
133
46
|
);
|
|
134
47
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import SuggestionItem from "../SuggestionItem";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { Badge, createStyles, Menu, Stack, Text } from "@mantine/core";
|
|
4
|
+
|
|
5
|
+
const MIN_LEFT_MARGIN = 5;
|
|
6
|
+
|
|
7
|
+
export type SuggestionGroupItemProps<T> = {
|
|
8
|
+
item: T;
|
|
9
|
+
index: number;
|
|
10
|
+
selectedIndex?: number;
|
|
11
|
+
clickItem: (item: T) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function SuggestionGroupItem<T extends SuggestionItem>(
|
|
15
|
+
props: SuggestionGroupItemProps<T>
|
|
16
|
+
) {
|
|
17
|
+
const itemRef = useRef<HTMLButtonElement>(null);
|
|
18
|
+
const { classes } = createStyles({ root: {} })(undefined, {
|
|
19
|
+
name: "SuggestionListItem",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function isSelected() {
|
|
23
|
+
const isKeyboardSelected =
|
|
24
|
+
props.selectedIndex !== undefined && props.selectedIndex === props.index;
|
|
25
|
+
const isMouseSelected = itemRef.current?.matches(":hover");
|
|
26
|
+
|
|
27
|
+
return isKeyboardSelected || isMouseSelected;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Updates HTML "data-hovered" attribute which Mantine uses to set mouse hover styles.
|
|
31
|
+
// Allows users to "hover" menu items when navigating using the keyboard.
|
|
32
|
+
function updateSelection() {
|
|
33
|
+
isSelected()
|
|
34
|
+
? itemRef.current?.setAttribute("data-hovered", "true")
|
|
35
|
+
: itemRef.current?.removeAttribute("data-hovered");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
// Updates whether the item is selected with the keyboard (triggered on selectedIndex prop change).
|
|
40
|
+
updateSelection();
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
isSelected() &&
|
|
44
|
+
itemRef.current &&
|
|
45
|
+
itemRef.current.getBoundingClientRect().left > MIN_LEFT_MARGIN //TODO: Kinda hacky, fix
|
|
46
|
+
// This check is needed because initially the menu is initialized somewhere above outside the screen (with left = 1)
|
|
47
|
+
// scrollIntoView() is called before the menu is set in the right place, and without the check would scroll to the top of the page every time
|
|
48
|
+
) {
|
|
49
|
+
itemRef.current.scrollIntoView({
|
|
50
|
+
behavior: "smooth",
|
|
51
|
+
block: "nearest",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const Icon = props.item.icon;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Menu.Item
|
|
60
|
+
className={classes.root}
|
|
61
|
+
icon={Icon && <Icon size={18} />}
|
|
62
|
+
onClick={() => props.clickItem(props.item)}
|
|
63
|
+
// Ensures an item selected with both mouse & keyboard doesn't get deselected on mouse leave.
|
|
64
|
+
onMouseLeave={() => {
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
updateSelection();
|
|
67
|
+
});
|
|
68
|
+
}}
|
|
69
|
+
ref={itemRef}
|
|
70
|
+
rightSection={
|
|
71
|
+
props.item.shortcut && <Badge size={"xs"}>{props.item.shortcut}</Badge>
|
|
72
|
+
}>
|
|
73
|
+
<Stack>
|
|
74
|
+
{/*Might need separate classes.*/}
|
|
75
|
+
<Text size={14} weight={500}>
|
|
76
|
+
{props.item.name}
|
|
77
|
+
</Text>
|
|
78
|
+
<Text size={10}>{props.item.hint}</Text>
|
|
79
|
+
</Stack>
|
|
80
|
+
</Menu.Item>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -1,39 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import styles from "./SuggestionList.module.css";
|
|
3
|
-
import rootStyles from "../../../../root.module.css";
|
|
1
|
+
import { createStyles, Menu } from "@mantine/core";
|
|
4
2
|
import { SuggestionGroup } from "./SuggestionGroup";
|
|
5
3
|
import SuggestionItem from "../SuggestionItem";
|
|
6
4
|
|
|
7
5
|
export type SuggestionListProps<T> = {
|
|
8
|
-
|
|
9
|
-
* Object containing all suggestion items, grouped by their `groupName`.
|
|
10
|
-
*/
|
|
6
|
+
// Object containing all suggestion items, grouped by their `groupName`.
|
|
11
7
|
groups: {
|
|
12
8
|
[groupName: string]: T[];
|
|
13
9
|
};
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
* The total number of suggestion-items
|
|
17
|
-
*/
|
|
11
|
+
//The total number of suggestion-items
|
|
18
12
|
count: number;
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
* This callback gets executed whenever an item is clicked on
|
|
22
|
-
*/
|
|
14
|
+
// This callback gets executed whenever an item is clicked on
|
|
23
15
|
onSelectItem: (item: T) => void;
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
* The index of the item that is currently selected (but not yet clicked on)
|
|
27
|
-
*/
|
|
17
|
+
// The index of the item that is currently selected (but not yet clicked on)
|
|
28
18
|
selectedIndex: number;
|
|
29
19
|
};
|
|
30
20
|
|
|
31
|
-
|
|
32
|
-
* Stateless component that renders the suggestion list
|
|
33
|
-
*/
|
|
21
|
+
// Stateless component that renders the suggestion list
|
|
34
22
|
export function SuggestionList<T extends SuggestionItem>(
|
|
35
23
|
props: SuggestionListProps<T>
|
|
36
24
|
) {
|
|
25
|
+
const { classes } = createStyles({ root: {} })(undefined, {
|
|
26
|
+
name: "SuggestionList",
|
|
27
|
+
});
|
|
28
|
+
|
|
37
29
|
const renderedGroups = [];
|
|
38
30
|
|
|
39
31
|
let currentGroupIndex = 0;
|
|
@@ -58,15 +50,24 @@ export function SuggestionList<T extends SuggestionItem>(
|
|
|
58
50
|
}
|
|
59
51
|
|
|
60
52
|
return (
|
|
61
|
-
<
|
|
62
|
-
|
|
53
|
+
<Menu
|
|
54
|
+
/** Hacky fix to get the desired menu behaviour. The trigger="hover"
|
|
55
|
+
* attribute allows focus to remain on the editor, allowing for suggestion
|
|
56
|
+
* filtering. The closeDelay=10000000 attribute allows the menu to stay open
|
|
57
|
+
* practically indefinitely, as normally hovering off it would cause it to
|
|
58
|
+
* close due to trigger="hover".
|
|
59
|
+
*/
|
|
60
|
+
defaultOpened={true}
|
|
61
|
+
trigger={"hover"}
|
|
62
|
+
closeDelay={10000000}>
|
|
63
|
+
<Menu.Dropdown className={classes.root}>
|
|
63
64
|
{renderedGroups.length > 0 ? (
|
|
64
65
|
renderedGroups
|
|
65
66
|
) : (
|
|
66
|
-
<
|
|
67
|
+
<Menu.Item>No match found</Menu.Item>
|
|
67
68
|
)}
|
|
68
|
-
</
|
|
69
|
-
</
|
|
69
|
+
</Menu.Dropdown>
|
|
70
|
+
</Menu>
|
|
70
71
|
|
|
71
72
|
// doesn't work well yet, maybe https://github.com/atomiks/tippyjs-react/issues/173
|
|
72
73
|
// We now render the tippy element manually in SuggestionListReactRenderer
|
package/src/useEditor.ts
CHANGED
|
@@ -17,6 +17,10 @@ const blockNoteOptions = {
|
|
|
17
17
|
enablePasteRules: true,
|
|
18
18
|
enableCoreExtensions: false,
|
|
19
19
|
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Main hook for importing a BlockNote editor into a react project
|
|
23
|
+
*/
|
|
20
24
|
export const useEditor = (
|
|
21
25
|
options: Partial<BlockNoteEditorOptions> = {},
|
|
22
26
|
deps: DependencyList = []
|