@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.
Files changed (143) hide show
  1. package/README.md +99 -0
  2. package/dist/blocknote.js +4485 -0
  3. package/dist/blocknote.js.map +1 -0
  4. package/dist/blocknote.umd.cjs +90 -0
  5. package/dist/blocknote.umd.cjs.map +1 -0
  6. package/dist/style.css +1 -0
  7. package/package.json +109 -0
  8. package/src/BlockNoteExtensions.ts +90 -0
  9. package/src/EditorContent.tsx +1 -0
  10. package/src/assets/inter-v12-latin/inter-v12-latin-100.woff +0 -0
  11. package/src/assets/inter-v12-latin/inter-v12-latin-100.woff2 +0 -0
  12. package/src/assets/inter-v12-latin/inter-v12-latin-200.woff +0 -0
  13. package/src/assets/inter-v12-latin/inter-v12-latin-200.woff2 +0 -0
  14. package/src/assets/inter-v12-latin/inter-v12-latin-300.woff +0 -0
  15. package/src/assets/inter-v12-latin/inter-v12-latin-300.woff2 +0 -0
  16. package/src/assets/inter-v12-latin/inter-v12-latin-500.woff +0 -0
  17. package/src/assets/inter-v12-latin/inter-v12-latin-500.woff2 +0 -0
  18. package/src/assets/inter-v12-latin/inter-v12-latin-600.woff +0 -0
  19. package/src/assets/inter-v12-latin/inter-v12-latin-600.woff2 +0 -0
  20. package/src/assets/inter-v12-latin/inter-v12-latin-700.woff +0 -0
  21. package/src/assets/inter-v12-latin/inter-v12-latin-700.woff2 +0 -0
  22. package/src/assets/inter-v12-latin/inter-v12-latin-800.woff +0 -0
  23. package/src/assets/inter-v12-latin/inter-v12-latin-800.woff2 +0 -0
  24. package/src/assets/inter-v12-latin/inter-v12-latin-900.woff +0 -0
  25. package/src/assets/inter-v12-latin/inter-v12-latin-900.woff2 +0 -0
  26. package/src/assets/inter-v12-latin/inter-v12-latin-regular.woff +0 -0
  27. package/src/assets/inter-v12-latin/inter-v12-latin-regular.woff2 +0 -0
  28. package/src/editor.module.css +3 -0
  29. package/src/extensions/Blocks/OrderedListPlugin.ts +46 -0
  30. package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +146 -0
  31. package/src/extensions/Blocks/commands/joinBackward.ts +274 -0
  32. package/src/extensions/Blocks/helpers/findBlock.ts +3 -0
  33. package/src/extensions/Blocks/helpers/setBlockHeading.ts +30 -0
  34. package/src/extensions/Blocks/index.ts +15 -0
  35. package/src/extensions/Blocks/nodes/Block.module.css +226 -0
  36. package/src/extensions/Blocks/nodes/Block.ts +390 -0
  37. package/src/extensions/Blocks/nodes/BlockGroup.ts +28 -0
  38. package/src/extensions/Blocks/nodes/Content.ts +50 -0
  39. package/src/extensions/Blocks/nodes/README.md +26 -0
  40. package/src/extensions/Blocks/rule.ts +48 -0
  41. package/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +28 -0
  42. package/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +245 -0
  43. package/src/extensions/BubbleMenu/component/BubbleMenu.tsx +216 -0
  44. package/src/extensions/BubbleMenu/component/DropdownBlockItem.module.css +13 -0
  45. package/src/extensions/BubbleMenu/component/DropdownBlockItem.tsx +25 -0
  46. package/src/extensions/BubbleMenu/component/LinkToolbarButton.tsx +67 -0
  47. package/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +15 -0
  48. package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx +266 -0
  49. package/src/extensions/DraggableBlocks/components/DragHandle.module.css +33 -0
  50. package/src/extensions/DraggableBlocks/components/DragHandle.tsx +108 -0
  51. package/src/extensions/DraggableBlocks/components/DragHandleMenu.module.css +10 -0
  52. package/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx +18 -0
  53. package/src/extensions/Hyperlinks/HyperlinkMark.tsx +16 -0
  54. package/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +200 -0
  55. package/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.tsx +59 -0
  56. package/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.tsx +72 -0
  57. package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.tsx +173 -0
  58. package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.ts +36 -0
  59. package/src/extensions/Hyperlinks/menus/atlaskit/README.md +1 -0
  60. package/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.tsx +61 -0
  61. package/src/extensions/Paragraph/FixedParagraph.ts +12 -0
  62. package/src/extensions/Placeholder/PlaceholderExtension.ts +127 -0
  63. package/src/extensions/SlashMenu/SlashMenuExtension.ts +43 -0
  64. package/src/extensions/SlashMenu/SlashMenuItem.ts +56 -0
  65. package/src/extensions/SlashMenu/defaultCommands.tsx +229 -0
  66. package/src/extensions/SlashMenu/index.ts +11 -0
  67. package/src/extensions/TrailingNode/TrailingNodeExtension.ts +70 -0
  68. package/src/extensions/UniqueID/UniqueID.ts +281 -0
  69. package/src/extensions/helpers/formatKeyboardShortcut.ts +9 -0
  70. package/src/fonts-inter.css +94 -0
  71. package/src/globals.css +28 -0
  72. package/src/index.ts +5 -0
  73. package/src/lib/atlaskit/browser.ts +47 -0
  74. package/src/root.module.css +19 -0
  75. package/src/shared/components/toolbar/SimpleToolbarButton.module.css +13 -0
  76. package/src/shared/components/toolbar/SimpleToolbarButton.tsx +56 -0
  77. package/src/shared/components/toolbar/Toolbar.module.css +10 -0
  78. package/src/shared/components/toolbar/Toolbar.tsx +5 -0
  79. package/src/shared/components/toolbar/ToolbarSeparator.module.css +13 -0
  80. package/src/shared/components/toolbar/ToolbarSeparator.tsx +7 -0
  81. package/src/shared/components/tooltip/TooltipContent.module.css +15 -0
  82. package/src/shared/components/tooltip/TooltipContent.tsx +23 -0
  83. package/src/shared/hooks/useEditorForceUpdate.tsx +30 -0
  84. package/src/shared/plugins/suggestion/SuggestionItem.ts +31 -0
  85. package/src/shared/plugins/suggestion/SuggestionListReactRenderer.ts +227 -0
  86. package/src/shared/plugins/suggestion/SuggestionPlugin.ts +365 -0
  87. package/src/shared/plugins/suggestion/components/SuggestionGroup.module.css +45 -0
  88. package/src/shared/plugins/suggestion/components/SuggestionGroup.tsx +134 -0
  89. package/src/shared/plugins/suggestion/components/SuggestionList.module.css +10 -0
  90. package/src/shared/plugins/suggestion/components/SuggestionList.tsx +91 -0
  91. package/src/style.css +7 -0
  92. package/src/useEditor.ts +47 -0
  93. package/src/vite-env.d.ts +1 -0
  94. package/types/src/BlockNoteExtensions.d.ts +4 -0
  95. package/types/src/EditorContent.d.ts +1 -0
  96. package/types/src/extensions/Blocks/OrderedListPlugin.d.ts +2 -0
  97. package/types/src/extensions/Blocks/PreviousBlockTypePlugin.d.ts +13 -0
  98. package/types/src/extensions/Blocks/commands/joinBackward.d.ts +14 -0
  99. package/types/src/extensions/Blocks/helpers/findBlock.d.ts +6 -0
  100. package/types/src/extensions/Blocks/helpers/setBlockHeading.d.ts +5 -0
  101. package/types/src/extensions/Blocks/index.d.ts +1 -0
  102. package/types/src/extensions/Blocks/nodes/Block.d.ts +32 -0
  103. package/types/src/extensions/Blocks/nodes/BlockGroup.d.ts +2 -0
  104. package/types/src/extensions/Blocks/nodes/Content.d.ts +5 -0
  105. package/types/src/extensions/Blocks/rule.d.ts +16 -0
  106. package/types/src/extensions/BubbleMenu/BubbleMenuExtension.d.ts +5 -0
  107. package/types/src/extensions/BubbleMenu/BubbleMenuPlugin.d.ts +46 -0
  108. package/types/src/extensions/BubbleMenu/component/BubbleMenu.d.ts +5 -0
  109. package/types/src/extensions/BubbleMenu/component/DropdownBlockItem.d.ts +10 -0
  110. package/types/src/extensions/BubbleMenu/component/LinkToolbarButton.d.ts +11 -0
  111. package/types/src/extensions/DraggableBlocks/DraggableBlocksExtension.d.ts +7 -0
  112. package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +18 -0
  113. package/types/src/extensions/DraggableBlocks/components/DragHandle.d.ts +12 -0
  114. package/types/src/extensions/DraggableBlocks/components/DragHandleMenu.d.ts +6 -0
  115. package/types/src/extensions/Hyperlinks/HyperlinkMark.d.ts +7 -0
  116. package/types/src/extensions/Hyperlinks/HyperlinkMenuPlugin.d.ts +2 -0
  117. package/types/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.d.ts +12 -0
  118. package/types/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.d.ts +10 -0
  119. package/types/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.d.ts +39 -0
  120. package/types/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.d.ts +1 -0
  121. package/types/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.d.ts +11 -0
  122. package/types/src/extensions/Paragraph/FixedParagraph.d.ts +1 -0
  123. package/types/src/extensions/Placeholder/PlaceholderExtension.d.ts +25 -0
  124. package/types/src/extensions/SlashMenu/SlashMenuExtension.d.ts +10 -0
  125. package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +43 -0
  126. package/types/src/extensions/SlashMenu/defaultCommands.d.ts +8 -0
  127. package/types/src/extensions/SlashMenu/index.d.ts +5 -0
  128. package/types/src/extensions/TrailingNode/TrailingNodeExtension.d.ts +10 -0
  129. package/types/src/extensions/UniqueID/UniqueID.d.ts +3 -0
  130. package/types/src/extensions/helpers/formatKeyboardShortcut.d.ts +1 -0
  131. package/types/src/index.d.ts +4 -0
  132. package/types/src/lib/atlaskit/browser.d.ts +12 -0
  133. package/types/src/shared/components/toolbar/SimpleToolbarButton.d.ts +16 -0
  134. package/types/src/shared/components/toolbar/Toolbar.d.ts +4 -0
  135. package/types/src/shared/components/toolbar/ToolbarSeparator.d.ts +2 -0
  136. package/types/src/shared/components/tooltip/TooltipContent.d.ts +15 -0
  137. package/types/src/shared/hooks/useEditorForceUpdate.d.ts +2 -0
  138. package/types/src/shared/plugins/suggestion/SuggestionItem.d.ts +29 -0
  139. package/types/src/shared/plugins/suggestion/SuggestionListReactRenderer.d.ts +71 -0
  140. package/types/src/shared/plugins/suggestion/SuggestionPlugin.d.ts +74 -0
  141. package/types/src/shared/plugins/suggestion/components/SuggestionGroup.d.ts +23 -0
  142. package/types/src/shared/plugins/suggestion/components/SuggestionList.d.ts +26 -0
  143. package/types/src/useEditor.d.ts +8 -0
@@ -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,5 @@
1
+ import "./globals.css";
2
+
3
+ export * from "./BlockNoteExtensions";
4
+ export * from "./EditorContent";
5
+ export * from "./useEditor";
@@ -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,13 @@
1
+ .icon {
2
+ width: 20px;
3
+ height: 20px;
4
+ fill: var(--N800);
5
+ }
6
+
7
+ .isSelected {
8
+ fill: white;
9
+ }
10
+
11
+ .isDisabled {
12
+ fill: gray;
13
+ }
@@ -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,10 @@
1
+ .toolbar {
2
+ color: var(--N800);
3
+ background-color: white;
4
+ border: 1px solid var(--N40);
5
+ box-shadow: 0px 4px 8px rgba(9, 30, 66, 0.15),
6
+ 0px 0px 1px rgba(9, 30, 66, 0.21);
7
+ border-radius: 4px;
8
+ font-size: 14px;
9
+ line-height: 1.42857142857143;
10
+ }
@@ -0,0 +1,5 @@
1
+ import styles from "./Toolbar.module.css";
2
+
3
+ export const Toolbar = (props: { children: any }) => {
4
+ return <div className={styles.toolbar}>{props.children}</div>;
5
+ };
@@ -0,0 +1,13 @@
1
+ .separator {
2
+ padding-left: 0.1em !important;
3
+ padding-right: 0.1em !important;
4
+ }
5
+ .separator::before {
6
+ content: "|";
7
+ color: lightgray;
8
+ }
9
+
10
+ .separator:hover {
11
+ cursor: auto !important;
12
+ background: white !important;
13
+ }
@@ -0,0 +1,7 @@
1
+ import styles from "./ToolbarSeparator.module.css";
2
+ import Button from "@atlaskit/button";
3
+
4
+ export const ToolbarSeparator = () => {
5
+ // return <span className={styles.separator}></span>;
6
+ return <Button appearance="subtle" className={styles.separator} />;
7
+ };
@@ -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
+ }