@blocknote/core 0.11.2 → 0.12.0

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 (128) hide show
  1. package/README.md +13 -17
  2. package/dist/blocknote.js +1600 -1403
  3. package/dist/blocknote.js.map +1 -1
  4. package/dist/blocknote.umd.cjs +6 -6
  5. package/dist/blocknote.umd.cjs.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/dist/webpack-stats.json +1 -1
  8. package/package.json +7 -3
  9. package/src/api/blockManipulation/blockManipulation.test.ts +19 -15
  10. package/src/api/blockManipulation/blockManipulation.ts +107 -17
  11. package/src/api/exporters/html/externalHTMLExporter.ts +3 -7
  12. package/src/api/exporters/html/htmlConversion.test.ts +6 -3
  13. package/src/api/exporters/html/internalHTMLSerializer.ts +3 -7
  14. package/src/api/exporters/html/util/sharedHTMLConversion.ts +3 -3
  15. package/src/api/exporters/markdown/markdownExporter.test.ts +7 -3
  16. package/src/api/exporters/markdown/markdownExporter.ts +2 -6
  17. package/src/api/nodeConversions/nodeConversions.test.ts +14 -7
  18. package/src/api/nodeConversions/nodeConversions.ts +1 -2
  19. package/src/api/parsers/html/parseHTML.test.ts +5 -1
  20. package/src/api/parsers/html/parseHTML.ts +2 -6
  21. package/src/api/parsers/html/util/nestedLists.ts +11 -1
  22. package/src/api/parsers/markdown/parseMarkdown.test.ts +3 -0
  23. package/src/api/parsers/markdown/parseMarkdown.ts +2 -6
  24. package/src/api/testUtil/cases/customBlocks.ts +18 -16
  25. package/src/api/testUtil/cases/customInlineContent.ts +12 -13
  26. package/src/api/testUtil/cases/customStyles.ts +12 -10
  27. package/src/api/testUtil/index.ts +4 -2
  28. package/src/api/testUtil/partialBlockTestUtil.ts +2 -6
  29. package/src/blocks/ImageBlockContent/ImageBlockContent.ts +1 -2
  30. package/src/blocks/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.ts +8 -1
  31. package/src/blocks/defaultBlockHelpers.ts +3 -3
  32. package/src/blocks/defaultBlockTypeGuards.ts +84 -0
  33. package/src/blocks/defaultBlocks.ts +29 -3
  34. package/src/editor/Block.css +2 -31
  35. package/src/editor/BlockNoteEditor.ts +218 -262
  36. package/src/editor/BlockNoteExtensions.ts +5 -2
  37. package/src/editor/BlockNoteSchema.ts +98 -0
  38. package/src/editor/BlockNoteTipTapEditor.ts +162 -0
  39. package/src/editor/cursorPositionTypes.ts +2 -6
  40. package/src/editor/editor.css +0 -1
  41. package/src/editor/selectionTypes.ts +2 -6
  42. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +22 -29
  43. package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +26 -27
  44. package/src/extensions/ImageToolbar/ImageToolbarPlugin.ts +45 -51
  45. package/src/extensions/Placeholder/PlaceholderExtension.ts +81 -88
  46. package/src/extensions/SideMenu/SideMenuPlugin.ts +55 -56
  47. package/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts +8 -0
  48. package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +353 -0
  49. package/src/extensions/{SlashMenu/defaultSlashMenuItems.ts → SuggestionMenu/getDefaultSlashMenuItems.ts} +119 -89
  50. package/src/extensions/TableHandles/TableHandlesPlugin.ts +62 -45
  51. package/src/extensions-shared/UiElementPosition.ts +4 -0
  52. package/src/index.ts +6 -6
  53. package/src/pm-nodes/BlockContainer.ts +5 -5
  54. package/src/schema/blocks/types.ts +15 -15
  55. package/src/schema/inlineContent/createSpec.ts +2 -2
  56. package/src/schema/inlineContent/types.ts +1 -1
  57. package/src/util/browser.ts +6 -4
  58. package/src/util/typescript.ts +7 -4
  59. package/types/src/api/blockManipulation/blockManipulation.d.ts +6 -1
  60. package/types/src/api/exporters/html/externalHTMLExporter.d.ts +2 -1
  61. package/types/src/api/exporters/html/internalHTMLSerializer.d.ts +2 -1
  62. package/types/src/api/exporters/markdown/markdownExporter.d.ts +2 -1
  63. package/types/src/api/nodeConversions/nodeConversions.d.ts +2 -1
  64. package/types/src/api/parsers/html/parseHTML.d.ts +2 -1
  65. package/types/src/api/parsers/markdown/parseMarkdown.d.ts +2 -1
  66. package/types/src/api/testUtil/cases/customBlocks.d.ts +72 -13
  67. package/types/src/api/testUtil/cases/customInlineContent.d.ts +281 -6
  68. package/types/src/api/testUtil/cases/customStyles.d.ts +247 -13
  69. package/types/src/api/testUtil/index.d.ts +4 -2
  70. package/types/src/api/testUtil/partialBlockTestUtil.d.ts +2 -1
  71. package/types/src/blocks/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.d.ts +6 -1
  72. package/types/src/blocks/defaultBlockHelpers.d.ts +2 -2
  73. package/types/src/blocks/defaultBlockTypeGuards.d.ts +24 -0
  74. package/types/src/blocks/defaultBlocks.d.ts +21 -15
  75. package/types/src/editor/BlockNoteEditor.d.ts +48 -53
  76. package/types/src/editor/BlockNoteExtensions.d.ts +1 -0
  77. package/types/src/editor/BlockNoteSchema.d.ts +34 -0
  78. package/types/src/editor/BlockNoteTipTapEditor.d.ts +28 -0
  79. package/types/src/editor/cursorPositionTypes.d.ts +2 -1
  80. package/types/src/editor/selectionTypes.d.ts +2 -1
  81. package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +5 -6
  82. package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.d.ts +2 -2
  83. package/types/src/extensions/ImageToolbar/ImageToolbarPlugin.d.ts +15 -14
  84. package/types/src/extensions/Placeholder/PlaceholderExtension.d.ts +2 -15
  85. package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +8 -7
  86. package/types/src/extensions/SuggestionMenu/DefaultSuggestionItem.d.ts +8 -0
  87. package/types/src/extensions/SuggestionMenu/SuggestionPlugin.d.ts +31 -0
  88. package/types/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.d.ts +10 -0
  89. package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +7 -7
  90. package/types/src/extensions-shared/UiElementPosition.d.ts +4 -0
  91. package/types/src/index.d.ts +6 -6
  92. package/types/src/pm-nodes/BlockContainer.d.ts +3 -2
  93. package/types/src/pm-nodes/BlockGroup.d.ts +1 -1
  94. package/types/src/schema/blocks/types.d.ts +15 -15
  95. package/types/src/schema/inlineContent/types.d.ts +1 -1
  96. package/types/src/util/browser.d.ts +1 -0
  97. package/types/src/util/typescript.d.ts +1 -0
  98. package/src/extensions/SlashMenu/BaseSlashMenuItem.ts +0 -12
  99. package/src/extensions/SlashMenu/SlashMenuPlugin.ts +0 -53
  100. package/src/extensions-shared/BaseUiElementTypes.ts +0 -8
  101. package/src/extensions-shared/README.md +0 -3
  102. package/src/extensions-shared/suggestion/SuggestionItem.ts +0 -3
  103. package/src/extensions-shared/suggestion/SuggestionPlugin.ts +0 -448
  104. package/types/src/extensions/SlashMenu/BaseSlashMenuItem.d.ts +0 -7
  105. package/types/src/extensions/SlashMenu/SlashMenuPlugin.d.ts +0 -13
  106. package/types/src/extensions/SlashMenu/defaultSlashMenuItems.d.ts +0 -3
  107. package/types/src/extensions-shared/BaseUiElementTypes.d.ts +0 -7
  108. package/types/src/extensions-shared/suggestion/SuggestionItem.d.ts +0 -3
  109. package/types/src/extensions-shared/suggestion/SuggestionPlugin.d.ts +0 -36
  110. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-100.woff +0 -0
  111. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-100.woff2 +0 -0
  112. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-200.woff +0 -0
  113. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-200.woff2 +0 -0
  114. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-300.woff +0 -0
  115. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-300.woff2 +0 -0
  116. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-500.woff +0 -0
  117. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-500.woff2 +0 -0
  118. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-600.woff +0 -0
  119. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-600.woff2 +0 -0
  120. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-700.woff +0 -0
  121. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-700.woff2 +0 -0
  122. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-800.woff +0 -0
  123. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-800.woff2 +0 -0
  124. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-900.woff +0 -0
  125. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-900.woff2 +0 -0
  126. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-regular.woff +0 -0
  127. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-regular.woff2 +0 -0
  128. /package/src/{assets/fonts-inter.css → fonts/inter.css} +0 -0
@@ -1,10 +1,10 @@
1
1
  /** Define the main block types **/
2
- import { Extension, Node } from "@tiptap/core";
2
+ import type { Extension, Node } from "@tiptap/core";
3
3
  import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
4
- import { InlineContent, InlineContentSchema, PartialInlineContent } from "../inlineContent/types";
5
- import { PropSchema, Props } from "../propTypes";
6
- import { StyleSchema } from "../styles/types";
7
- export type BlockNoteDOMElement = "editor" | "blockContainer" | "blockGroup" | "blockContent" | "inlineContent";
4
+ import type { InlineContent, InlineContentSchema, PartialInlineContent } from "../inlineContent/types";
5
+ import type { PropSchema, Props } from "../propTypes";
6
+ import type { StyleSchema } from "../styles/types";
7
+ export type BlockNoteDOMElement = "editor" | "block" | "blockGroup" | "blockContent" | "inlineContent";
8
8
  export type BlockNoteDOMAttributes = Partial<{
9
9
  [DOMElement in BlockNoteDOMElement]: Record<string, string>;
10
10
  }>;
@@ -17,13 +17,13 @@ export type TiptapBlockImplementation<T extends BlockConfig, B extends BlockSche
17
17
  requiredExtensions?: Array<Extension | Node>;
18
18
  node: Node;
19
19
  toInternalHTML: (block: BlockFromConfigNoChildren<T, I, S> & {
20
- children: Block<B, I, S>[];
20
+ children: BlockNoDefaults<B, I, S>[];
21
21
  }, editor: BlockNoteEditor<B, I, S>) => {
22
22
  dom: HTMLElement;
23
23
  contentDOM?: HTMLElement;
24
24
  };
25
25
  toExternalHTML: (block: BlockFromConfigNoChildren<T, I, S> & {
26
- children: Block<B, I, S>[];
26
+ children: BlockNoDefaults<B, I, S>[];
27
27
  }, editor: BlockNoteEditor<B, I, S>) => {
28
28
  dom: HTMLElement;
29
29
  contentDOM?: HTMLElement;
@@ -60,16 +60,16 @@ export type BlockFromConfigNoChildren<B extends BlockConfig, I extends InlineCon
60
60
  content: B["content"] extends "inline" ? InlineContent<I, S>[] : B["content"] extends "table" ? TableContent<I, S> : B["content"] extends "none" ? undefined : never;
61
61
  };
62
62
  export type BlockFromConfig<B extends BlockConfig, I extends InlineContentSchema, S extends StyleSchema> = BlockFromConfigNoChildren<B, I, S> & {
63
- children: Block<BlockSchema, I, S>[];
63
+ children: BlockNoDefaults<BlockSchema, I, S>[];
64
64
  };
65
65
  type BlocksWithoutChildren<BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema> = {
66
66
  [BType in keyof BSchema]: BlockFromConfigNoChildren<BSchema[BType], I, S>;
67
67
  };
68
- export type Block<BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema> = BlocksWithoutChildren<BSchema, I, S>[keyof BSchema] & {
69
- children: Block<BSchema, I, S>[];
68
+ export type BlockNoDefaults<BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema> = BlocksWithoutChildren<BSchema, I, S>[keyof BSchema] & {
69
+ children: BlockNoDefaults<BSchema, I, S>[];
70
70
  };
71
71
  export type SpecificBlock<BSchema extends BlockSchema, BType extends keyof BSchema, I extends InlineContentSchema, S extends StyleSchema> = BlocksWithoutChildren<BSchema, I, S>[BType] & {
72
- children: Block<BSchema, I, S>[];
72
+ children: BlockNoDefaults<BSchema, I, S>[];
73
73
  };
74
74
  /** CODE FOR PARTIAL BLOCKS, analogous to above
75
75
  *
@@ -92,14 +92,14 @@ type PartialBlockFromConfigNoChildren<B extends BlockConfig, I extends InlineCon
92
92
  type PartialBlocksWithoutChildren<BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema> = {
93
93
  [BType in keyof BSchema]: PartialBlockFromConfigNoChildren<BSchema[BType], I, S>;
94
94
  };
95
- export type PartialBlock<BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema> = PartialBlocksWithoutChildren<BSchema, I, S>[keyof PartialBlocksWithoutChildren<BSchema, I, S>] & Partial<{
96
- children: PartialBlock<BSchema, I, S>[];
95
+ export type PartialBlockNoDefaults<BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema> = PartialBlocksWithoutChildren<BSchema, I, S>[keyof PartialBlocksWithoutChildren<BSchema, I, S>] & Partial<{
96
+ children: PartialBlockNoDefaults<BSchema, I, S>[];
97
97
  }>;
98
98
  export type SpecificPartialBlock<BSchema extends BlockSchema, I extends InlineContentSchema, BType extends keyof BSchema, S extends StyleSchema> = PartialBlocksWithoutChildren<BSchema, I, S>[BType] & {
99
- children?: Block<BSchema, I, S>[];
99
+ children?: BlockNoDefaults<BSchema, I, S>[];
100
100
  };
101
101
  export type PartialBlockFromConfig<B extends BlockConfig, I extends InlineContentSchema, S extends StyleSchema> = PartialBlockFromConfigNoChildren<B, I, S> & {
102
- children?: Block<BlockSchema, I, S>[];
102
+ children?: BlockNoDefaults<BlockSchema, I, S>[];
103
103
  };
104
104
  export type BlockIdentifier = {
105
105
  id: string;
@@ -37,7 +37,7 @@ export type InlineContentFromConfig<I extends InlineContentConfig, S extends Sty
37
37
  export type PartialCustomInlineContentFromConfig<I extends CustomInlineContentConfig, S extends StyleSchema> = {
38
38
  type: I["type"];
39
39
  props?: Props<I["propSchema"]>;
40
- content: I["content"] extends "styled" ? StyledText<S>[] | string : I["content"] extends "plain" ? string : I["content"] extends "none" ? undefined : never;
40
+ content?: I["content"] extends "styled" ? StyledText<S>[] | string : I["content"] extends "plain" ? string : I["content"] extends "none" ? undefined : never;
41
41
  };
42
42
  export type PartialInlineContentFromConfig<I extends InlineContentConfig, S extends StyleSchema> = I extends "text" ? string | StyledText<S> : I extends "link" ? PartialLink<S> : I extends CustomInlineContentConfig ? PartialCustomInlineContentFromConfig<I, S> : never;
43
43
  export type StyledText<T extends StyleSchema> = {
@@ -1,3 +1,4 @@
1
1
  export declare const isAppleOS: () => boolean;
2
2
  export declare function formatKeyboardShortcut(shortcut: string): string;
3
3
  export declare function mergeCSSClasses(...classes: string[]): string;
4
+ export declare const isSafari: () => boolean;
@@ -1,3 +1,4 @@
1
1
  export declare class UnreachableCaseError extends Error {
2
2
  constructor(val: never);
3
3
  }
4
+ export type NoInfer<T> = [T][T extends any ? 0 : never];
@@ -1,12 +0,0 @@
1
- import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
2
- import { SuggestionItem } from "../../extensions-shared/suggestion/SuggestionItem";
3
- import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
4
-
5
- export type BaseSlashMenuItem<
6
- BSchema extends BlockSchema,
7
- I extends InlineContentSchema,
8
- S extends StyleSchema
9
- > = SuggestionItem & {
10
- execute: (editor: BlockNoteEditor<BSchema, I, S>) => void;
11
- aliases?: string[];
12
- };
@@ -1,53 +0,0 @@
1
- import { Plugin, PluginKey } from "prosemirror-state";
2
-
3
- import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
4
- import {
5
- SuggestionsMenuState,
6
- setupSuggestionsMenu,
7
- } from "../../extensions-shared/suggestion/SuggestionPlugin";
8
- import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
9
- import { EventEmitter } from "../../util/EventEmitter";
10
- import { BaseSlashMenuItem } from "./BaseSlashMenuItem";
11
-
12
- export const slashMenuPluginKey = new PluginKey("SlashMenuPlugin");
13
-
14
- export class SlashMenuProsemirrorPlugin<
15
- BSchema extends BlockSchema,
16
- I extends InlineContentSchema,
17
- S extends StyleSchema,
18
- SlashMenuItem extends BaseSlashMenuItem<BSchema, I, S>
19
- > extends EventEmitter<any> {
20
- public readonly plugin: Plugin;
21
- public readonly itemCallback: (item: SlashMenuItem) => void;
22
-
23
- constructor(editor: BlockNoteEditor<BSchema, I, S>, items: SlashMenuItem[]) {
24
- super();
25
- const suggestions = setupSuggestionsMenu<SlashMenuItem, BSchema, I, S>(
26
- editor,
27
- (state) => {
28
- this.emit("update", state);
29
- },
30
- slashMenuPluginKey,
31
- "/",
32
- (query) =>
33
- items.filter(
34
- ({ name, aliases }: SlashMenuItem) =>
35
- name.toLowerCase().startsWith(query.toLowerCase()) ||
36
- (aliases &&
37
- aliases.filter((alias) =>
38
- alias.toLowerCase().startsWith(query.toLowerCase())
39
- ).length !== 0)
40
- ),
41
- ({ item, editor }) => item.execute(editor)
42
- );
43
-
44
- this.plugin = suggestions.plugin;
45
- this.itemCallback = suggestions.itemCallback;
46
- }
47
-
48
- public onUpdate(
49
- callback: (state: SuggestionsMenuState<SlashMenuItem>) => void
50
- ) {
51
- return this.on("update", callback);
52
- }
53
- }
@@ -1,8 +0,0 @@
1
- export type BaseUiElementCallbacks = {
2
- destroy: () => void;
3
- };
4
-
5
- export type BaseUiElementState = {
6
- show: boolean;
7
- referencePos: DOMRect;
8
- };
@@ -1,3 +0,0 @@
1
- ### @blocknote/core/src/extensions-shared
2
-
3
- Helper functions / base plugins for @blocknote/core/src/extensions
@@ -1,3 +0,0 @@
1
- export type SuggestionItem = {
2
- name: string;
3
- };
@@ -1,448 +0,0 @@
1
- import { findParentNode } from "@tiptap/core";
2
- import { EditorState, Plugin, PluginKey } from "prosemirror-state";
3
- import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
4
- import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
5
- import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
6
- import { BaseUiElementState } from "../BaseUiElementTypes";
7
- import { SuggestionItem } from "./SuggestionItem";
8
-
9
- const findBlock = findParentNode((node) => node.type.name === "blockContainer");
10
-
11
- export type SuggestionsMenuState<T extends SuggestionItem> =
12
- BaseUiElementState & {
13
- // The suggested items to display.
14
- filteredItems: T[];
15
- // The index of the suggested item that's currently hovered by the keyboard.
16
- keyboardHoveredItemIndex: number;
17
- };
18
-
19
- class SuggestionsMenuView<
20
- T extends SuggestionItem,
21
- BSchema extends BlockSchema,
22
- I extends InlineContentSchema,
23
- S extends StyleSchema
24
- > {
25
- private suggestionsMenuState?: SuggestionsMenuState<T>;
26
- public updateSuggestionsMenu: () => void;
27
-
28
- pluginState: SuggestionPluginState<T>;
29
-
30
- constructor(
31
- private readonly editor: BlockNoteEditor<BSchema, I, S>,
32
- private readonly pluginKey: PluginKey,
33
- updateSuggestionsMenu: (
34
- suggestionsMenuState: SuggestionsMenuState<T>
35
- ) => void = () => {
36
- // noop
37
- }
38
- ) {
39
- this.pluginState = getDefaultPluginState<T>();
40
-
41
- this.updateSuggestionsMenu = () => {
42
- if (!this.suggestionsMenuState) {
43
- throw new Error("Attempting to update uninitialized suggestions menu");
44
- }
45
-
46
- updateSuggestionsMenu(this.suggestionsMenuState);
47
- };
48
-
49
- document.addEventListener("scroll", this.handleScroll);
50
- }
51
-
52
- handleScroll = () => {
53
- if (this.suggestionsMenuState?.show) {
54
- const decorationNode = document.querySelector(
55
- `[data-decoration-id="${this.pluginState.decorationId}"]`
56
- );
57
- this.suggestionsMenuState.referencePos =
58
- decorationNode!.getBoundingClientRect();
59
- this.updateSuggestionsMenu();
60
- }
61
- };
62
-
63
- update(view: EditorView, prevState: EditorState) {
64
- const prev = this.pluginKey.getState(prevState);
65
- const next = this.pluginKey.getState(view.state);
66
-
67
- // See how the state changed
68
- const started = !prev.active && next.active;
69
- const stopped = prev.active && !next.active;
70
- // TODO: Currently also true for cases in which an update isn't needed so selected list item index updates still
71
- // cause the view to update. May need to be more strict.
72
- const changed = prev.active && next.active;
73
-
74
- // Cancel when suggestion isn't active
75
- if (!started && !changed && !stopped) {
76
- return;
77
- }
78
-
79
- this.pluginState = stopped ? prev : next;
80
-
81
- if (stopped || !this.editor.isEditable) {
82
- this.suggestionsMenuState!.show = false;
83
- this.updateSuggestionsMenu();
84
-
85
- return;
86
- }
87
-
88
- const decorationNode = document.querySelector(
89
- `[data-decoration-id="${this.pluginState.decorationId}"]`
90
- );
91
-
92
- if (this.editor.isEditable) {
93
- this.suggestionsMenuState = {
94
- show: true,
95
- referencePos: decorationNode!.getBoundingClientRect(),
96
- filteredItems: this.pluginState.items,
97
- keyboardHoveredItemIndex: this.pluginState.keyboardHoveredItemIndex!,
98
- };
99
-
100
- this.updateSuggestionsMenu();
101
- }
102
- }
103
-
104
- destroy() {
105
- document.removeEventListener("scroll", this.handleScroll);
106
- }
107
- }
108
-
109
- type SuggestionPluginState<T extends SuggestionItem> = {
110
- // True when the menu is shown, false when hidden.
111
- active: boolean;
112
- // The character that triggered the menu being shown. Allowing the trigger to be different to the default
113
- // trigger allows other extensions to open it programmatically.
114
- triggerCharacter: string | undefined;
115
- // The editor position just after the trigger character, i.e. where the user query begins. Used to figure out
116
- // which menu items to show and can also be used to delete the trigger character.
117
- queryStartPos: number | undefined;
118
- // The items that should be shown in the menu.
119
- items: T[];
120
- // The index of the item in the menu that's currently hovered using the keyboard.
121
- keyboardHoveredItemIndex: number | undefined;
122
- // The number of characters typed after the last query that matched with at least 1 item. Used to close the
123
- // menu if the user keeps entering queries that don't return any results.
124
- notFoundCount: number | undefined;
125
- decorationId: string | undefined;
126
- };
127
-
128
- function getDefaultPluginState<
129
- T extends SuggestionItem
130
- >(): SuggestionPluginState<T> {
131
- return {
132
- active: false,
133
- triggerCharacter: undefined,
134
- queryStartPos: undefined,
135
- items: [] as T[],
136
- keyboardHoveredItemIndex: undefined,
137
- notFoundCount: 0,
138
- decorationId: undefined,
139
- };
140
- }
141
-
142
- /**
143
- * A ProseMirror plugin for suggestions, designed to make '/'-commands possible as well as mentions.
144
- *
145
- * This is basically a simplified version of TipTap's [Suggestions](https://github.com/ueberdosis/tiptap/tree/db92a9b313c5993b723c85cd30256f1d4a0b65e1/packages/suggestion) plugin.
146
- *
147
- * This version is adapted from the aforementioned version in the following ways:
148
- * - This version supports generic items instead of only strings (to allow for more advanced filtering for example)
149
- * - This version hides some unnecessary complexity from the user of the plugin.
150
- * - This version handles key events differently
151
- */
152
- export const setupSuggestionsMenu = <
153
- T extends SuggestionItem,
154
- BSchema extends BlockSchema,
155
- I extends InlineContentSchema,
156
- S extends StyleSchema
157
- >(
158
- editor: BlockNoteEditor<BSchema, I, S>,
159
- updateSuggestionsMenu: (
160
- suggestionsMenuState: SuggestionsMenuState<T>
161
- ) => void,
162
-
163
- pluginKey: PluginKey,
164
- defaultTriggerCharacter: string,
165
- items: (query: string) => T[] = () => [],
166
- onSelectItem: (props: {
167
- item: T;
168
- editor: BlockNoteEditor<BSchema, I, S>;
169
- }) => void = () => {
170
- // noop
171
- }
172
- ) => {
173
- // Assertions
174
- if (defaultTriggerCharacter.length !== 1) {
175
- throw new Error("'char' should be a single character");
176
- }
177
-
178
- let suggestionsPluginView: SuggestionsMenuView<T, BSchema, I, S>;
179
-
180
- const deactivate = (view: EditorView) => {
181
- view.dispatch(view.state.tr.setMeta(pluginKey, { deactivate: true }));
182
- };
183
-
184
- return {
185
- plugin: new Plugin({
186
- key: pluginKey,
187
-
188
- view: () => {
189
- suggestionsPluginView = new SuggestionsMenuView<T, BSchema, I, S>(
190
- editor,
191
- pluginKey,
192
-
193
- updateSuggestionsMenu
194
- );
195
- return suggestionsPluginView;
196
- },
197
-
198
- state: {
199
- // Initialize the plugin's internal state.
200
- init(): SuggestionPluginState<T> {
201
- return getDefaultPluginState<T>();
202
- },
203
-
204
- // Apply changes to the plugin state from an editor transaction.
205
- apply(transaction, prev, oldState, newState): SuggestionPluginState<T> {
206
- // TODO: More clearly define which transactions should be ignored.
207
- if (transaction.getMeta("orderedListIndexing") !== undefined) {
208
- return prev;
209
- }
210
-
211
- // Checks if the menu should be shown.
212
- if (transaction.getMeta(pluginKey)?.activate) {
213
- return {
214
- active: true,
215
- triggerCharacter:
216
- transaction.getMeta(pluginKey)?.triggerCharacter || "",
217
- queryStartPos: newState.selection.from,
218
- items: items(""),
219
- keyboardHoveredItemIndex: 0,
220
- // TODO: Maybe should be 1 if the menu has no possible items? Probably redundant since a menu with no items
221
- // is useless in practice.
222
- notFoundCount: 0,
223
- decorationId: `id_${Math.floor(Math.random() * 0xffffffff)}`,
224
- };
225
- }
226
-
227
- // Checks if the menu is hidden, in which case it doesn't need to be hidden or updated.
228
- if (!prev.active) {
229
- return prev;
230
- }
231
-
232
- const next = { ...prev };
233
-
234
- // Updates which menu items to show by checking which items the current query (the text between the trigger
235
- // character and caret) matches with.
236
- next.items = items(
237
- newState.doc.textBetween(
238
- prev.queryStartPos!,
239
- newState.selection.from
240
- )
241
- );
242
-
243
- // Updates notFoundCount if the query doesn't match any items.
244
- next.notFoundCount = 0;
245
- if (next.items.length === 0) {
246
- // Checks how many characters were typed or deleted since the last transaction, and updates the notFoundCount
247
- // accordingly. Also ensures the notFoundCount does not become negative.
248
- next.notFoundCount = Math.max(
249
- 0,
250
- prev.notFoundCount! +
251
- (newState.selection.from - oldState.selection.from)
252
- );
253
- }
254
-
255
- // Hides the menu. This is done after items and notFoundCount are already updated as notFoundCount is needed to
256
- // check if the menu should be hidden.
257
- if (
258
- // Highlighting text should hide the menu.
259
- newState.selection.from !== newState.selection.to ||
260
- // Transactions with plugin metadata {deactivate: true} should hide the menu.
261
- transaction.getMeta(pluginKey)?.deactivate ||
262
- // Certain mouse events should hide the menu.
263
- // TODO: Change to global mousedown listener.
264
- transaction.getMeta("focus") ||
265
- transaction.getMeta("blur") ||
266
- transaction.getMeta("pointer") ||
267
- // Moving the caret before the character which triggered the menu should hide it.
268
- (prev.active && newState.selection.from < prev.queryStartPos!) ||
269
- // Entering more than 3 characters, after the last query that matched with at least 1 menu item, should hide
270
- // the menu.
271
- next.notFoundCount > 3
272
- ) {
273
- return getDefaultPluginState<T>();
274
- }
275
-
276
- // Updates keyboardHoveredItemIndex if the up or down arrow key was
277
- // pressed, or resets it if the keyboard cursor moved.
278
- if (
279
- transaction.getMeta(pluginKey)?.selectedItemIndexChanged !==
280
- undefined
281
- ) {
282
- let newIndex =
283
- transaction.getMeta(pluginKey).selectedItemIndexChanged;
284
-
285
- // Allows selection to jump between first and last items.
286
- if (newIndex < 0) {
287
- newIndex = prev.items.length - 1;
288
- } else if (newIndex >= prev.items.length) {
289
- newIndex = 0;
290
- }
291
-
292
- next.keyboardHoveredItemIndex = newIndex;
293
- } else if (oldState.selection.from !== newState.selection.from) {
294
- next.keyboardHoveredItemIndex = 0;
295
- }
296
-
297
- return next;
298
- },
299
- },
300
-
301
- props: {
302
- handleKeyDown(view, event) {
303
- const menuIsActive = (this as Plugin).getState(view.state).active;
304
-
305
- // Shows the menu if the default trigger character was pressed and the menu isn't active.
306
- if (event.key === defaultTriggerCharacter && !menuIsActive) {
307
- view.dispatch(
308
- view.state.tr
309
- .insertText(defaultTriggerCharacter)
310
- .scrollIntoView()
311
- .setMeta(pluginKey, {
312
- activate: true,
313
- triggerCharacter: defaultTriggerCharacter,
314
- })
315
- );
316
-
317
- return true;
318
- }
319
-
320
- // Doesn't handle other keystrokes if the menu isn't active.
321
- if (!menuIsActive) {
322
- return false;
323
- }
324
-
325
- // Handles keystrokes for navigating the menu.
326
- const {
327
- triggerCharacter,
328
- queryStartPos,
329
- items,
330
- keyboardHoveredItemIndex,
331
- } = pluginKey.getState(view.state);
332
-
333
- // Moves the keyboard selection to the previous item.
334
- if (event.key === "ArrowUp") {
335
- view.dispatch(
336
- view.state.tr.setMeta(pluginKey, {
337
- selectedItemIndexChanged: keyboardHoveredItemIndex - 1,
338
- })
339
- );
340
- return true;
341
- }
342
-
343
- // Moves the keyboard selection to the next item.
344
- if (event.key === "ArrowDown") {
345
- view.dispatch(
346
- view.state.tr.setMeta(pluginKey, {
347
- selectedItemIndexChanged: keyboardHoveredItemIndex + 1,
348
- })
349
- );
350
- return true;
351
- }
352
-
353
- // Selects an item and closes the menu.
354
- if (event.key === "Enter") {
355
- if (items.length === 0) {
356
- return true;
357
- }
358
-
359
- deactivate(view);
360
- editor._tiptapEditor
361
- .chain()
362
- .focus()
363
- .deleteRange({
364
- from: queryStartPos! - triggerCharacter!.length,
365
- to: editor._tiptapEditor.state.selection.from,
366
- })
367
- .run();
368
-
369
- onSelectItem({
370
- item: items[keyboardHoveredItemIndex],
371
- editor: editor,
372
- });
373
-
374
- return true;
375
- }
376
-
377
- // Closes the menu.
378
- if (event.key === "Escape") {
379
- deactivate(view);
380
- return true;
381
- }
382
-
383
- return false;
384
- },
385
-
386
- // Setup decorator on the currently active suggestion.
387
- decorations(state) {
388
- const { active, decorationId, queryStartPos, triggerCharacter } = (
389
- this as Plugin
390
- ).getState(state);
391
-
392
- if (!active) {
393
- return null;
394
- }
395
-
396
- // If the menu was opened programmatically by another extension, it may not use a trigger character. In this
397
- // case, the decoration is set on the whole block instead, as the decoration range would otherwise be empty.
398
- if (triggerCharacter === "") {
399
- const blockNode = findBlock(state.selection);
400
- if (blockNode) {
401
- return DecorationSet.create(state.doc, [
402
- Decoration.node(
403
- blockNode.pos,
404
- blockNode.pos + blockNode.node.nodeSize,
405
- {
406
- nodeName: "span",
407
- class: "bn-suggestion-decorator",
408
- "data-decoration-id": decorationId,
409
- }
410
- ),
411
- ]);
412
- }
413
- }
414
- // Creates an inline decoration around the trigger character.
415
- return DecorationSet.create(state.doc, [
416
- Decoration.inline(
417
- queryStartPos - triggerCharacter.length,
418
- queryStartPos,
419
- {
420
- nodeName: "span",
421
- class: "bn-suggestion-decorator",
422
- "data-decoration-id": decorationId,
423
- }
424
- ),
425
- ]);
426
- },
427
- },
428
- }),
429
- itemCallback: (item: T) => {
430
- deactivate(editor._tiptapEditor.view);
431
- editor._tiptapEditor
432
- .chain()
433
- .focus()
434
- .deleteRange({
435
- from:
436
- suggestionsPluginView.pluginState.queryStartPos! -
437
- suggestionsPluginView.pluginState.triggerCharacter!.length,
438
- to: editor._tiptapEditor.state.selection.from,
439
- })
440
- .run();
441
-
442
- onSelectItem({
443
- item: item,
444
- editor: editor,
445
- });
446
- },
447
- };
448
- };
@@ -1,7 +0,0 @@
1
- import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
2
- import { SuggestionItem } from "../../extensions-shared/suggestion/SuggestionItem";
3
- import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
4
- export type BaseSlashMenuItem<BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema> = SuggestionItem & {
5
- execute: (editor: BlockNoteEditor<BSchema, I, S>) => void;
6
- aliases?: string[];
7
- };
@@ -1,13 +0,0 @@
1
- import { Plugin, PluginKey } from "prosemirror-state";
2
- import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
3
- import { SuggestionsMenuState } from "../../extensions-shared/suggestion/SuggestionPlugin";
4
- import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
5
- import { EventEmitter } from "../../util/EventEmitter";
6
- import { BaseSlashMenuItem } from "./BaseSlashMenuItem";
7
- export declare const slashMenuPluginKey: PluginKey<any>;
8
- export declare class SlashMenuProsemirrorPlugin<BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema, SlashMenuItem extends BaseSlashMenuItem<BSchema, I, S>> extends EventEmitter<any> {
9
- readonly plugin: Plugin;
10
- readonly itemCallback: (item: SlashMenuItem) => void;
11
- constructor(editor: BlockNoteEditor<BSchema, I, S>, items: SlashMenuItem[]);
12
- onUpdate(callback: (state: SuggestionsMenuState<SlashMenuItem>) => void): () => void;
13
- }
@@ -1,3 +0,0 @@
1
- import { InlineContentSchema, StyleSchema } from "../../schema";
2
- import { BaseSlashMenuItem } from "./BaseSlashMenuItem";
3
- export declare const getDefaultSlashMenuItems: <BSchema extends Record<string, import("../../schema").BlockConfig>, I extends InlineContentSchema, S extends StyleSchema>(schema?: BSchema) => BaseSlashMenuItem<BSchema, I, S>[];
@@ -1,7 +0,0 @@
1
- export type BaseUiElementCallbacks = {
2
- destroy: () => void;
3
- };
4
- export type BaseUiElementState = {
5
- show: boolean;
6
- referencePos: DOMRect;
7
- };
@@ -1,3 +0,0 @@
1
- export type SuggestionItem = {
2
- name: string;
3
- };