@blocknote/core 0.1.0-alpha.3
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/README.md +99 -0
- package/dist/blocknote.js +4485 -0
- package/dist/blocknote.js.map +1 -0
- package/dist/blocknote.umd.cjs +90 -0
- package/dist/blocknote.umd.cjs.map +1 -0
- package/dist/style.css +1 -0
- package/package.json +109 -0
- package/src/BlockNoteExtensions.ts +90 -0
- package/src/EditorContent.tsx +1 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-100.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-100.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-200.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-200.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-300.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-300.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-500.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-500.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-600.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-600.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-700.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-700.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-800.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-800.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-900.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-900.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-regular.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-regular.woff2 +0 -0
- package/src/editor.module.css +3 -0
- package/src/extensions/Blocks/OrderedListPlugin.ts +46 -0
- package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +146 -0
- package/src/extensions/Blocks/commands/joinBackward.ts +274 -0
- package/src/extensions/Blocks/helpers/findBlock.ts +3 -0
- package/src/extensions/Blocks/helpers/setBlockHeading.ts +30 -0
- package/src/extensions/Blocks/index.ts +15 -0
- package/src/extensions/Blocks/nodes/Block.module.css +226 -0
- package/src/extensions/Blocks/nodes/Block.ts +390 -0
- package/src/extensions/Blocks/nodes/BlockGroup.ts +28 -0
- package/src/extensions/Blocks/nodes/Content.ts +50 -0
- package/src/extensions/Blocks/nodes/README.md +26 -0
- package/src/extensions/Blocks/rule.ts +48 -0
- package/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +28 -0
- package/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +245 -0
- package/src/extensions/BubbleMenu/component/BubbleMenu.tsx +216 -0
- package/src/extensions/BubbleMenu/component/DropdownBlockItem.module.css +13 -0
- package/src/extensions/BubbleMenu/component/DropdownBlockItem.tsx +25 -0
- package/src/extensions/BubbleMenu/component/LinkToolbarButton.tsx +67 -0
- package/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +15 -0
- package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx +266 -0
- package/src/extensions/DraggableBlocks/components/DragHandle.module.css +33 -0
- package/src/extensions/DraggableBlocks/components/DragHandle.tsx +108 -0
- package/src/extensions/DraggableBlocks/components/DragHandleMenu.module.css +10 -0
- package/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx +18 -0
- package/src/extensions/Hyperlinks/HyperlinkMark.tsx +16 -0
- package/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +200 -0
- package/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.tsx +59 -0
- package/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.tsx +72 -0
- package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.tsx +173 -0
- package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.ts +36 -0
- package/src/extensions/Hyperlinks/menus/atlaskit/README.md +1 -0
- package/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.tsx +61 -0
- package/src/extensions/Paragraph/FixedParagraph.ts +12 -0
- package/src/extensions/Placeholder/PlaceholderExtension.ts +127 -0
- package/src/extensions/SlashMenu/SlashMenuExtension.ts +43 -0
- package/src/extensions/SlashMenu/SlashMenuItem.ts +56 -0
- package/src/extensions/SlashMenu/defaultCommands.tsx +229 -0
- package/src/extensions/SlashMenu/index.ts +11 -0
- package/src/extensions/TrailingNode/TrailingNodeExtension.ts +70 -0
- package/src/extensions/UniqueID/UniqueID.ts +281 -0
- package/src/extensions/helpers/formatKeyboardShortcut.ts +9 -0
- package/src/fonts-inter.css +94 -0
- package/src/globals.css +28 -0
- package/src/index.ts +5 -0
- package/src/lib/atlaskit/browser.ts +47 -0
- package/src/root.module.css +19 -0
- package/src/shared/components/toolbar/SimpleToolbarButton.module.css +13 -0
- package/src/shared/components/toolbar/SimpleToolbarButton.tsx +56 -0
- package/src/shared/components/toolbar/Toolbar.module.css +10 -0
- package/src/shared/components/toolbar/Toolbar.tsx +5 -0
- package/src/shared/components/toolbar/ToolbarSeparator.module.css +13 -0
- package/src/shared/components/toolbar/ToolbarSeparator.tsx +7 -0
- package/src/shared/components/tooltip/TooltipContent.module.css +15 -0
- package/src/shared/components/tooltip/TooltipContent.tsx +23 -0
- package/src/shared/hooks/useEditorForceUpdate.tsx +30 -0
- package/src/shared/plugins/suggestion/SuggestionItem.ts +31 -0
- package/src/shared/plugins/suggestion/SuggestionListReactRenderer.ts +227 -0
- package/src/shared/plugins/suggestion/SuggestionPlugin.ts +365 -0
- package/src/shared/plugins/suggestion/components/SuggestionGroup.module.css +45 -0
- package/src/shared/plugins/suggestion/components/SuggestionGroup.tsx +134 -0
- package/src/shared/plugins/suggestion/components/SuggestionList.module.css +10 -0
- package/src/shared/plugins/suggestion/components/SuggestionList.tsx +91 -0
- package/src/style.css +7 -0
- package/src/useEditor.ts +47 -0
- package/src/vite-env.d.ts +1 -0
- package/types/src/BlockNoteExtensions.d.ts +4 -0
- package/types/src/EditorContent.d.ts +1 -0
- package/types/src/extensions/Blocks/OrderedListPlugin.d.ts +2 -0
- package/types/src/extensions/Blocks/PreviousBlockTypePlugin.d.ts +13 -0
- package/types/src/extensions/Blocks/commands/joinBackward.d.ts +14 -0
- package/types/src/extensions/Blocks/helpers/findBlock.d.ts +6 -0
- package/types/src/extensions/Blocks/helpers/setBlockHeading.d.ts +5 -0
- package/types/src/extensions/Blocks/index.d.ts +1 -0
- package/types/src/extensions/Blocks/nodes/Block.d.ts +32 -0
- package/types/src/extensions/Blocks/nodes/BlockGroup.d.ts +2 -0
- package/types/src/extensions/Blocks/nodes/Content.d.ts +5 -0
- package/types/src/extensions/Blocks/rule.d.ts +16 -0
- package/types/src/extensions/BubbleMenu/BubbleMenuExtension.d.ts +5 -0
- package/types/src/extensions/BubbleMenu/BubbleMenuPlugin.d.ts +46 -0
- package/types/src/extensions/BubbleMenu/component/BubbleMenu.d.ts +5 -0
- package/types/src/extensions/BubbleMenu/component/DropdownBlockItem.d.ts +10 -0
- package/types/src/extensions/BubbleMenu/component/LinkToolbarButton.d.ts +11 -0
- package/types/src/extensions/DraggableBlocks/DraggableBlocksExtension.d.ts +7 -0
- package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +18 -0
- package/types/src/extensions/DraggableBlocks/components/DragHandle.d.ts +12 -0
- package/types/src/extensions/DraggableBlocks/components/DragHandleMenu.d.ts +6 -0
- package/types/src/extensions/Hyperlinks/HyperlinkMark.d.ts +7 -0
- package/types/src/extensions/Hyperlinks/HyperlinkMenuPlugin.d.ts +2 -0
- package/types/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.d.ts +12 -0
- package/types/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.d.ts +10 -0
- package/types/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.d.ts +39 -0
- package/types/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.d.ts +1 -0
- package/types/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.d.ts +11 -0
- package/types/src/extensions/Paragraph/FixedParagraph.d.ts +1 -0
- package/types/src/extensions/Placeholder/PlaceholderExtension.d.ts +25 -0
- package/types/src/extensions/SlashMenu/SlashMenuExtension.d.ts +10 -0
- package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +43 -0
- package/types/src/extensions/SlashMenu/defaultCommands.d.ts +8 -0
- package/types/src/extensions/SlashMenu/index.d.ts +5 -0
- package/types/src/extensions/TrailingNode/TrailingNodeExtension.d.ts +10 -0
- package/types/src/extensions/UniqueID/UniqueID.d.ts +3 -0
- package/types/src/extensions/helpers/formatKeyboardShortcut.d.ts +1 -0
- package/types/src/index.d.ts +4 -0
- package/types/src/lib/atlaskit/browser.d.ts +12 -0
- package/types/src/shared/components/toolbar/SimpleToolbarButton.d.ts +16 -0
- package/types/src/shared/components/toolbar/Toolbar.d.ts +4 -0
- package/types/src/shared/components/toolbar/ToolbarSeparator.d.ts +2 -0
- package/types/src/shared/components/tooltip/TooltipContent.d.ts +15 -0
- package/types/src/shared/hooks/useEditorForceUpdate.d.ts +2 -0
- package/types/src/shared/plugins/suggestion/SuggestionItem.d.ts +29 -0
- package/types/src/shared/plugins/suggestion/SuggestionListReactRenderer.d.ts +71 -0
- package/types/src/shared/plugins/suggestion/SuggestionPlugin.d.ts +74 -0
- package/types/src/shared/plugins/suggestion/components/SuggestionGroup.d.ts +23 -0
- package/types/src/shared/plugins/suggestion/components/SuggestionList.d.ts +26 -0
- package/types/src/useEditor.d.ts +8 -0
package/src/globals.css
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
@import url("fonts-inter.css");
|
|
2
|
+
|
|
3
|
+
/* TODO: should not be on root as this changes entire consuming application */
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
/* Define a set of colors to be used throughout the app for consistency
|
|
7
|
+
see https://atlassian.design/foundations/color for more info */
|
|
8
|
+
--N800: #172b4d; /* Dark neutral used for tooltips and text on light background */
|
|
9
|
+
--N40: #dfe1e6; /* Light neutral used for subtle borders and text on dark background */
|
|
10
|
+
|
|
11
|
+
font-family: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont,
|
|
12
|
+
"Open Sans", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell",
|
|
13
|
+
"Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
|
14
|
+
-webkit-font-smoothing: antialiased;
|
|
15
|
+
-moz-osx-font-smoothing: grayscale;
|
|
16
|
+
|
|
17
|
+
color: rgb(60, 65, 73);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
button {
|
|
21
|
+
font-family: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont,
|
|
22
|
+
"Open Sans", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell",
|
|
23
|
+
"Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
|
24
|
+
-webkit-font-smoothing: antialiased;
|
|
25
|
+
-moz-osx-font-smoothing: grayscale;
|
|
26
|
+
|
|
27
|
+
color: rgb(60, 65, 73);
|
|
28
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// from atlaskit/editor-common
|
|
2
|
+
const result = {
|
|
3
|
+
mac: false,
|
|
4
|
+
ie: false,
|
|
5
|
+
ie_version: 0,
|
|
6
|
+
gecko: false,
|
|
7
|
+
chrome: false,
|
|
8
|
+
chrome_version: 0,
|
|
9
|
+
android: false,
|
|
10
|
+
ios: false,
|
|
11
|
+
webkit: false,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
if (typeof navigator !== "undefined") {
|
|
15
|
+
const ieEdge = /Edge\/(\d+)/.exec(navigator.userAgent);
|
|
16
|
+
const ieUpTo10 = /MSIE \d/.test(navigator.userAgent);
|
|
17
|
+
const ie11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(
|
|
18
|
+
navigator.userAgent
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
result.mac = /Mac/.test(navigator.platform);
|
|
22
|
+
let ie = (result.ie = !!(ieUpTo10 || ie11up || ieEdge));
|
|
23
|
+
result.ie_version = ieUpTo10
|
|
24
|
+
? (document as any).documentMode || 6
|
|
25
|
+
: ie11up
|
|
26
|
+
? +ie11up[1]
|
|
27
|
+
: ieEdge
|
|
28
|
+
? +ieEdge[1]
|
|
29
|
+
: null;
|
|
30
|
+
result.gecko = !ie && /gecko\/\d/i.test(navigator.userAgent);
|
|
31
|
+
result.chrome = !ie && /Chrome\//.test(navigator.userAgent);
|
|
32
|
+
result.chrome_version = parseInt(
|
|
33
|
+
(navigator.userAgent.match(/Chrome\/(\d{2})/) || [])[1],
|
|
34
|
+
10
|
|
35
|
+
);
|
|
36
|
+
result.android = /Android \d/.test(navigator.userAgent);
|
|
37
|
+
result.ios =
|
|
38
|
+
!ie &&
|
|
39
|
+
/AppleWebKit/.test(navigator.userAgent) &&
|
|
40
|
+
/Mobile\/\w+/.test(navigator.userAgent);
|
|
41
|
+
result.webkit =
|
|
42
|
+
!ie &&
|
|
43
|
+
!!document.documentElement &&
|
|
44
|
+
"WebkitAppearance" in document.documentElement.style;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default result;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
bnRoot should be applied to all top-level elements
|
|
3
|
+
|
|
4
|
+
This includes the Prosemirror editor, but also <div> element such as
|
|
5
|
+
Tippy popups that are appended to document.body directly
|
|
6
|
+
*/
|
|
7
|
+
.bnRoot {
|
|
8
|
+
-webkit-box-sizing: border-box;
|
|
9
|
+
-moz-box-sizing: border-box;
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.bnRoot *,
|
|
14
|
+
.bnRoot *::before,
|
|
15
|
+
.bnRoot *::after {
|
|
16
|
+
-webkit-box-sizing: inherit;
|
|
17
|
+
-moz-box-sizing: inherit;
|
|
18
|
+
box-sizing: inherit;
|
|
19
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import Button from "@atlaskit/button";
|
|
2
|
+
import Tippy from "@tippyjs/react";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import styles from "./SimpleToolbarButton.module.css";
|
|
5
|
+
import { TooltipContent } from "../tooltip/TooltipContent";
|
|
6
|
+
import React from "react";
|
|
7
|
+
|
|
8
|
+
export type SimpleToolbarButtonProps = {
|
|
9
|
+
onClick?: (e: React.MouseEvent) => void;
|
|
10
|
+
icon?: React.ComponentType<{ className: string }>;
|
|
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 SimpleToolbarButton = forwardRef(
|
|
22
|
+
(props: SimpleToolbarButtonProps, ref) => {
|
|
23
|
+
const ButtonIcon = props.icon;
|
|
24
|
+
return (
|
|
25
|
+
<Tippy
|
|
26
|
+
content={
|
|
27
|
+
<TooltipContent
|
|
28
|
+
mainTooltip={props.mainTooltip}
|
|
29
|
+
secondaryTooltip={props.secondaryTooltip}
|
|
30
|
+
/>
|
|
31
|
+
}>
|
|
32
|
+
<Button
|
|
33
|
+
ref={ref as any}
|
|
34
|
+
appearance="subtle"
|
|
35
|
+
onClick={props.onClick}
|
|
36
|
+
isSelected={props.isSelected || false}
|
|
37
|
+
isDisabled={props.isDisabled || false}
|
|
38
|
+
iconBefore={
|
|
39
|
+
ButtonIcon && (
|
|
40
|
+
<ButtonIcon
|
|
41
|
+
className={
|
|
42
|
+
styles.icon +
|
|
43
|
+
" " +
|
|
44
|
+
(props.isSelected ? styles.isSelected : "") +
|
|
45
|
+
" " +
|
|
46
|
+
(props.isDisabled ? styles.isDisabled : "")
|
|
47
|
+
}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
}>
|
|
51
|
+
{props.children}
|
|
52
|
+
</Button>
|
|
53
|
+
</Tippy>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.tooltip {
|
|
2
|
+
color: var(--N40);
|
|
3
|
+
background-color: var(--N800);
|
|
4
|
+
box-shadow: 0 0 10px rgba(253, 254, 255, 0.8),
|
|
5
|
+
0 0 3px rgba(253, 254, 255, 0.4);
|
|
6
|
+
border-radius: 2px;
|
|
7
|
+
font-size: smaller;
|
|
8
|
+
text-align: center;
|
|
9
|
+
padding: 4px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.secondaryText {
|
|
13
|
+
font-weight: 400;
|
|
14
|
+
opacity: 0.6;
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import styles from "./TooltipContent.module.css";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper for the tooltip for inline bubble menu buttons.
|
|
5
|
+
*
|
|
6
|
+
* Often used to display a tooltip showing the command name + keyboard shortcut, e.g.:
|
|
7
|
+
*
|
|
8
|
+
* Bold
|
|
9
|
+
* Ctrl+B
|
|
10
|
+
*
|
|
11
|
+
* TODO: maybe use default Tippy styles instead?
|
|
12
|
+
*/
|
|
13
|
+
export const TooltipContent = (props: {
|
|
14
|
+
mainTooltip: string;
|
|
15
|
+
secondaryTooltip?: string;
|
|
16
|
+
}) => (
|
|
17
|
+
<div className={styles.tooltip}>
|
|
18
|
+
<div>{props.mainTooltip}</div>
|
|
19
|
+
{props.secondaryTooltip && (
|
|
20
|
+
<div className={styles.secondaryText}>{props.secondaryTooltip}</div>
|
|
21
|
+
)}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Editor } from "@tiptap/core";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
function useForceUpdate() {
|
|
5
|
+
const [, setValue] = useState(0);
|
|
6
|
+
|
|
7
|
+
return () => setValue((value) => value + 1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// This is a component that is similar to https://github.com/ueberdosis/tiptap/blob/main/packages/react/src/useEditor.ts
|
|
11
|
+
// Use it to rerender a component whenever a transaction happens in the editor
|
|
12
|
+
export const useEditorForceUpdate = (editor: Editor) => {
|
|
13
|
+
const forceUpdate = useForceUpdate();
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const callback = () => {
|
|
17
|
+
requestAnimationFrame(() => {
|
|
18
|
+
requestAnimationFrame(() => {
|
|
19
|
+
forceUpdate();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
editor.on("transaction", callback);
|
|
25
|
+
return () => {
|
|
26
|
+
editor.off("transaction", callback);
|
|
27
|
+
};
|
|
28
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
29
|
+
}, [editor]);
|
|
30
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A generic interface used in all suggestion menus (slash menu, mentions, etc)
|
|
3
|
+
*/
|
|
4
|
+
export default interface SuggestionItem {
|
|
5
|
+
/**
|
|
6
|
+
* The name of the item
|
|
7
|
+
*/
|
|
8
|
+
name: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The name of the group to which this item belongs
|
|
12
|
+
*/
|
|
13
|
+
groupName: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The react icon
|
|
17
|
+
*/
|
|
18
|
+
icon?: React.ComponentType<{ className: string }>;
|
|
19
|
+
|
|
20
|
+
hint?: string;
|
|
21
|
+
|
|
22
|
+
shortcut?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* This function matches this item against a query string, the function should return **true** if the item
|
|
26
|
+
* matches the query or **false** otherwise.
|
|
27
|
+
*
|
|
28
|
+
* @param query the query string
|
|
29
|
+
*/
|
|
30
|
+
match(query: string): boolean;
|
|
31
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { Editor as ReactEditor, ReactRenderer } from "@tiptap/react";
|
|
2
|
+
import { Editor } from "@tiptap/core";
|
|
3
|
+
import tippy, { Instance } from "tippy.js";
|
|
4
|
+
import SuggestionItem from "./SuggestionItem";
|
|
5
|
+
import {
|
|
6
|
+
SuggestionList,
|
|
7
|
+
SuggestionListProps,
|
|
8
|
+
} from "./components/SuggestionList";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The interface that each suggestion renderer should conform to.
|
|
12
|
+
*/
|
|
13
|
+
export interface SuggestionRenderer<T extends SuggestionItem> {
|
|
14
|
+
/**
|
|
15
|
+
* Disposes of the suggestion menu.
|
|
16
|
+
*/
|
|
17
|
+
onExit?: (props: SuggestionRendererProps<T>) => void;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Updates the suggestion menu.
|
|
21
|
+
*
|
|
22
|
+
* This function should be called when the renderer's `props` change,
|
|
23
|
+
* after `onStart` has been called.
|
|
24
|
+
*/
|
|
25
|
+
onUpdate?: (props: SuggestionRendererProps<T>) => void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates and displays a new suggestion menu popup.
|
|
29
|
+
*/
|
|
30
|
+
onStart?: (props: SuggestionRendererProps<T>) => void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Function for handling key events
|
|
34
|
+
*/
|
|
35
|
+
onKeyDown?: (event: KeyboardEvent) => boolean;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The DOM Element representing the suggestion menu
|
|
39
|
+
*/
|
|
40
|
+
getComponent: () => Element | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type SuggestionRendererProps<T extends SuggestionItem> = {
|
|
44
|
+
/**
|
|
45
|
+
* Object containing all suggestion items, grouped by their `groupName`.
|
|
46
|
+
*/
|
|
47
|
+
groups: {
|
|
48
|
+
[groupName: string]: T[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The total number of suggestion-items.
|
|
53
|
+
*/
|
|
54
|
+
count: number;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* This callback is executed whenever the user selects an item.
|
|
58
|
+
*
|
|
59
|
+
* @param item the selected item
|
|
60
|
+
*/
|
|
61
|
+
onSelectItem: (item: T) => void;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A function returning the client rect to use as reference for positioning the suggestion menu popup.
|
|
65
|
+
*/
|
|
66
|
+
clientRect: (() => DOMRect) | null;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* This callback is executed when the suggestion menu needs to be closed,
|
|
70
|
+
* e.g. when the user presses escape.
|
|
71
|
+
*/
|
|
72
|
+
onClose: () => void;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* This function creates a SuggestionRenderer based on TipTap's ReactRenderer utility.
|
|
77
|
+
*
|
|
78
|
+
* The resulting renderer can be used to display a suggestion menu containing (grouped) suggestion items.
|
|
79
|
+
*
|
|
80
|
+
* This renderer also takes care of the following key events:
|
|
81
|
+
* - Key up/down, for navigating the suggestion menu (selecting different items)
|
|
82
|
+
* - Enter for picking the currently selected item and closing the menu
|
|
83
|
+
* - Escape to close the menu, without taking action
|
|
84
|
+
*
|
|
85
|
+
* @param editor the TipTap editor
|
|
86
|
+
* @returns the newly constructed SuggestionRenderer
|
|
87
|
+
*/
|
|
88
|
+
export default function createRenderer<T extends SuggestionItem>(
|
|
89
|
+
editor: Editor
|
|
90
|
+
): SuggestionRenderer<T> {
|
|
91
|
+
let component: ReactRenderer;
|
|
92
|
+
let popup: Instance[];
|
|
93
|
+
let componentsDisposedOrDisposing = true;
|
|
94
|
+
let selectedIndex = 0;
|
|
95
|
+
let props: SuggestionRendererProps<T> | undefined;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Helper function to find out what item corresponds to a certain index.
|
|
99
|
+
*
|
|
100
|
+
* This function might throw an error if the index is invalid,
|
|
101
|
+
* or when this function is not called in the proper environment.
|
|
102
|
+
*
|
|
103
|
+
* @param index the index
|
|
104
|
+
* @returns the item that corresponds to the index
|
|
105
|
+
*/
|
|
106
|
+
const itemByIndex = (index: number): T => {
|
|
107
|
+
if (!props) {
|
|
108
|
+
throw new Error("props not set");
|
|
109
|
+
}
|
|
110
|
+
let currentIndex = 0;
|
|
111
|
+
for (const groupName in props.groups) {
|
|
112
|
+
const items = props.groups[groupName];
|
|
113
|
+
const groupSize = items.length;
|
|
114
|
+
// Check if index lies within this group
|
|
115
|
+
if (index < currentIndex + groupSize) {
|
|
116
|
+
return items[index - currentIndex];
|
|
117
|
+
}
|
|
118
|
+
currentIndex += groupSize;
|
|
119
|
+
}
|
|
120
|
+
throw Error("item not found");
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
getComponent: () => {
|
|
125
|
+
if (!popup || !popup[0]) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
// return the tippy container element, this is used to ensure
|
|
129
|
+
// that click events inside the menu are handled properly.
|
|
130
|
+
return popup[0].reference;
|
|
131
|
+
},
|
|
132
|
+
onStart: (newProps) => {
|
|
133
|
+
props = newProps;
|
|
134
|
+
componentsDisposedOrDisposing = false;
|
|
135
|
+
selectedIndex = 0;
|
|
136
|
+
const componentProps: SuggestionListProps<T> = {
|
|
137
|
+
groups: newProps.groups,
|
|
138
|
+
count: newProps.count,
|
|
139
|
+
onSelectItem: newProps.onSelectItem,
|
|
140
|
+
selectedIndex,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
component = new ReactRenderer(SuggestionList as any, {
|
|
144
|
+
editor: editor as ReactEditor,
|
|
145
|
+
props: componentProps,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
popup = tippy("body", {
|
|
149
|
+
getReferenceClientRect: newProps.clientRect,
|
|
150
|
+
appendTo: () => document.body,
|
|
151
|
+
content: component.element,
|
|
152
|
+
showOnCreate: true,
|
|
153
|
+
interactive: true,
|
|
154
|
+
trigger: "manual",
|
|
155
|
+
placement: "bottom-start",
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
onUpdate: (newProps) => {
|
|
160
|
+
props = newProps;
|
|
161
|
+
if (props.groups !== component.props.groups) {
|
|
162
|
+
// if the set of items is different (e.g.: by typing / searching), reset the selectedIndex to 0
|
|
163
|
+
selectedIndex = 0;
|
|
164
|
+
}
|
|
165
|
+
const componentProps: SuggestionListProps<T> = {
|
|
166
|
+
groups: props.groups,
|
|
167
|
+
count: props.count,
|
|
168
|
+
onSelectItem: props.onSelectItem,
|
|
169
|
+
selectedIndex,
|
|
170
|
+
};
|
|
171
|
+
component.updateProps(componentProps);
|
|
172
|
+
|
|
173
|
+
popup[0].setProps({
|
|
174
|
+
getReferenceClientRect: props.clientRect,
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
onKeyDown: (event) => {
|
|
179
|
+
if (!props) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (event.key === "ArrowUp") {
|
|
183
|
+
selectedIndex = (selectedIndex + props.count - 1) % props.count;
|
|
184
|
+
component.updateProps({
|
|
185
|
+
selectedIndex,
|
|
186
|
+
});
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (event.key === "ArrowDown") {
|
|
191
|
+
selectedIndex = (selectedIndex + 1) % props.count;
|
|
192
|
+
component.updateProps({
|
|
193
|
+
selectedIndex,
|
|
194
|
+
});
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (event.key === "Enter") {
|
|
199
|
+
const item = itemByIndex(selectedIndex);
|
|
200
|
+
props.onSelectItem(item);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (event.key === "Escape") {
|
|
205
|
+
props.onClose();
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
onExit: (_props) => {
|
|
212
|
+
if (componentsDisposedOrDisposing) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// onExit, first hide tippy popup so it shows fade-out
|
|
216
|
+
// then (after 1 second, actually destroy resources)
|
|
217
|
+
componentsDisposedOrDisposing = true;
|
|
218
|
+
const popupToDestroy = popup[0];
|
|
219
|
+
const componentToDestroy = component;
|
|
220
|
+
popupToDestroy.hide();
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
popupToDestroy.destroy();
|
|
223
|
+
componentToDestroy.destroy();
|
|
224
|
+
}, 1000);
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|