@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
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { Editor, Range } from "@tiptap/core";
|
|
2
|
+
import { escapeRegExp, groupBy } from "lodash";
|
|
3
|
+
import { Plugin, PluginKey, Selection } from "prosemirror-state";
|
|
4
|
+
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
5
|
+
import { findBlock } from "../../../extensions/Blocks/helpers/findBlock";
|
|
6
|
+
import SuggestionItem from "./SuggestionItem";
|
|
7
|
+
|
|
8
|
+
import createRenderer, {
|
|
9
|
+
SuggestionRendererProps,
|
|
10
|
+
} from "./SuggestionListReactRenderer";
|
|
11
|
+
|
|
12
|
+
export type SuggestionPluginOptions<T extends SuggestionItem> = {
|
|
13
|
+
/**
|
|
14
|
+
* The name of the plugin.
|
|
15
|
+
*
|
|
16
|
+
* Used for ensuring that the plugin key is unique when more than one instance of the SuggestionPlugin is used.
|
|
17
|
+
*/
|
|
18
|
+
pluginKey: PluginKey;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The TipTap editor.
|
|
22
|
+
*/
|
|
23
|
+
editor: Editor;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The character that should trigger the suggestion menu to pop up (e.g. a '/' for commands)
|
|
27
|
+
*/
|
|
28
|
+
char: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The callback that gets executed when an item is selected by the user.
|
|
32
|
+
*
|
|
33
|
+
* **NOTE:** The command text is not removed automatically from the editor by this plugin,
|
|
34
|
+
* this should be done manually. The `editor` and `range` properties passed
|
|
35
|
+
* to the callback function might come in handy when doing this.
|
|
36
|
+
*/
|
|
37
|
+
onSelectItem?: (props: { item: T; editor: Editor; range: Range }) => void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A function that should supply the plugin with items to suggest, based on a certain query string.
|
|
41
|
+
*/
|
|
42
|
+
items?: (query: string) => T[];
|
|
43
|
+
|
|
44
|
+
allow?: (props: { editor: Editor; range: Range }) => boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type MenuType = "slash" | "drag";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Finds a command: a specified character (e.g. '/') followed by a string of characters (all characters except the specified character are allowed).
|
|
51
|
+
* Returns the string following the specified character or undefined if no command was found.
|
|
52
|
+
*
|
|
53
|
+
* @param char the character that indicates the start of a command
|
|
54
|
+
* @param selection the selection (only works if the selection is empty; i.e. is a blinking cursor).
|
|
55
|
+
* @returns an object containing the matching word (excluding the specified character) and the range of the match (including the specified character) or undefined if there is no match.
|
|
56
|
+
*/
|
|
57
|
+
export function findCommandBeforeCursor(
|
|
58
|
+
char: string,
|
|
59
|
+
selection: Selection
|
|
60
|
+
): { range: Range; query: string } | undefined {
|
|
61
|
+
if (!selection.empty) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// get the text before the cursor as a node
|
|
66
|
+
const node = selection.$anchor.nodeBefore;
|
|
67
|
+
if (!node || !node.text) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// regex to match anything between with the specified char (e.g. '/') and the end of text (which is the end of selection)
|
|
72
|
+
const regex = new RegExp(`${escapeRegExp(char)}([^${escapeRegExp(char)}]*)$`);
|
|
73
|
+
const match = node.text.match(regex);
|
|
74
|
+
|
|
75
|
+
if (!match) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
query: match[1],
|
|
81
|
+
range: {
|
|
82
|
+
from: selection.$anchor.pos - match[1].length - char.length,
|
|
83
|
+
to: selection.$anchor.pos,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A ProseMirror plugin for suggestions, designed to make '/'-commands possible as well as mentions.
|
|
90
|
+
*
|
|
91
|
+
* This is basically a simplified version of TipTap's [Suggestions](https://github.com/ueberdosis/tiptap/tree/db92a9b313c5993b723c85cd30256f1d4a0b65e1/packages/suggestion) plugin.
|
|
92
|
+
*
|
|
93
|
+
* This version is adapted from the aforementioned version in the following ways:
|
|
94
|
+
* - This version supports generic items instead of only strings (to allow for more advanced filtering for example)
|
|
95
|
+
* - This version hides some unnecessary complexity from the user of the plugin.
|
|
96
|
+
* - This version handles key events differently
|
|
97
|
+
*
|
|
98
|
+
* @param options options for configuring the plugin
|
|
99
|
+
* @returns the prosemirror plugin
|
|
100
|
+
*/
|
|
101
|
+
export function createSuggestionPlugin<T extends SuggestionItem>({
|
|
102
|
+
pluginKey,
|
|
103
|
+
editor,
|
|
104
|
+
char,
|
|
105
|
+
onSelectItem: selectItemCallback = () => {},
|
|
106
|
+
items = () => [],
|
|
107
|
+
}: SuggestionPluginOptions<T>) {
|
|
108
|
+
// Assertions
|
|
109
|
+
if (char.length !== 1) {
|
|
110
|
+
throw new Error("'char' should be a single character");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const renderer = createRenderer<T>(editor);
|
|
114
|
+
|
|
115
|
+
// Plugin key is passed in parameter so it can be exported and used in draghandle
|
|
116
|
+
return new Plugin({
|
|
117
|
+
key: pluginKey,
|
|
118
|
+
|
|
119
|
+
filterTransaction(transaction) {
|
|
120
|
+
// prevent blurring when clicking with the mouse inside the popup menu
|
|
121
|
+
const blurMeta = transaction.getMeta("blur");
|
|
122
|
+
if (blurMeta?.event.relatedTarget) {
|
|
123
|
+
const c = renderer.getComponent();
|
|
124
|
+
if (c?.contains(blurMeta.event.relatedTarget)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
view() {
|
|
132
|
+
return {
|
|
133
|
+
update: async (view, prevState) => {
|
|
134
|
+
const prev = this.key?.getState(prevState);
|
|
135
|
+
const next = this.key?.getState(view.state);
|
|
136
|
+
|
|
137
|
+
// See how the state changed
|
|
138
|
+
const started = !prev.active && next.active;
|
|
139
|
+
const stopped = prev.active && !next.active;
|
|
140
|
+
const changed = !started && !stopped && prev.query !== next.query;
|
|
141
|
+
|
|
142
|
+
// Cancel when suggestion isn't active
|
|
143
|
+
if (!started && !changed && !stopped) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const state = stopped ? prev : next;
|
|
148
|
+
const decorationNode = document.querySelector(
|
|
149
|
+
`[data-decoration-id="${state.decorationId}"]`
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const groups: { [groupName: string]: T[] } = groupBy(
|
|
153
|
+
state.items,
|
|
154
|
+
"groupName"
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const deactivate = () => {
|
|
158
|
+
view.dispatch(
|
|
159
|
+
view.state.tr.setMeta(pluginKey, { deactivate: true })
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const rendererProps: SuggestionRendererProps<T> = {
|
|
164
|
+
groups: changed || started ? groups : {},
|
|
165
|
+
count: state.items.length,
|
|
166
|
+
onSelectItem: (item: T) => {
|
|
167
|
+
deactivate();
|
|
168
|
+
selectItemCallback({
|
|
169
|
+
item,
|
|
170
|
+
editor,
|
|
171
|
+
range: state.range,
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
// virtual node for popper.js or tippy.js
|
|
175
|
+
// this can be used for building popups without a DOM node
|
|
176
|
+
clientRect: decorationNode
|
|
177
|
+
? () => decorationNode.getBoundingClientRect()
|
|
178
|
+
: null,
|
|
179
|
+
onClose: () => {
|
|
180
|
+
deactivate();
|
|
181
|
+
renderer.onExit?.(rendererProps);
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (stopped) {
|
|
186
|
+
renderer.onExit?.(rendererProps);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (changed) {
|
|
190
|
+
renderer.onUpdate?.(rendererProps);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (started) {
|
|
194
|
+
renderer.onStart?.(rendererProps);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
state: {
|
|
201
|
+
// Initialize the plugin's internal state.
|
|
202
|
+
init() {
|
|
203
|
+
return {
|
|
204
|
+
active: false,
|
|
205
|
+
range: {} as any, // TODO
|
|
206
|
+
query: null as string | null,
|
|
207
|
+
notFoundCount: 0,
|
|
208
|
+
items: [] as T[],
|
|
209
|
+
type: "slash",
|
|
210
|
+
decorationId: null as string | null,
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
// Apply changes to the plugin state from a view transaction.
|
|
215
|
+
apply(transaction, prev, _oldState, _newState) {
|
|
216
|
+
const { selection } = transaction;
|
|
217
|
+
const next = { ...prev };
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
// only show popup if selection is a blinking cursor
|
|
221
|
+
selection.from === selection.to &&
|
|
222
|
+
// deactivate popup from view (e.g.: choice has been made or esc has been pressed)
|
|
223
|
+
!transaction.getMeta(pluginKey)?.deactivate &&
|
|
224
|
+
// deactivate because a mouse event occurs (user clicks somewhere else in the document)
|
|
225
|
+
!transaction.getMeta("focus") &&
|
|
226
|
+
!transaction.getMeta("blur") &&
|
|
227
|
+
!transaction.getMeta("pointer")
|
|
228
|
+
) {
|
|
229
|
+
// Reset active state if we just left the previous suggestion range (e.g.: key arrows moving before /)
|
|
230
|
+
if (prev.active && selection.from <= prev.range.from) {
|
|
231
|
+
next.active = false;
|
|
232
|
+
} else if (transaction.getMeta(pluginKey)?.activate) {
|
|
233
|
+
// Start showing suggestions. activate has been set after typing a "/" (or whatever the specified character is), so let's create the decoration and initialize
|
|
234
|
+
const newDecorationId = `id_${Math.floor(
|
|
235
|
+
Math.random() * 0xffffffff
|
|
236
|
+
)}`;
|
|
237
|
+
next.decorationId = newDecorationId;
|
|
238
|
+
next.range = {
|
|
239
|
+
from: selection.from - 1,
|
|
240
|
+
to: selection.to,
|
|
241
|
+
};
|
|
242
|
+
next.query = "";
|
|
243
|
+
next.active = true;
|
|
244
|
+
next.type = transaction.getMeta(pluginKey)?.type;
|
|
245
|
+
} else if (prev.active) {
|
|
246
|
+
// Try to match against where our cursor currently is
|
|
247
|
+
// if the type is slash we get the command after the character
|
|
248
|
+
// otherwise we get the whole query
|
|
249
|
+
const match = findCommandBeforeCursor(
|
|
250
|
+
prev.type === "slash" ? char : "",
|
|
251
|
+
selection
|
|
252
|
+
);
|
|
253
|
+
if (!match) {
|
|
254
|
+
throw new Error("active but no match (suggestions)");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
next.range = match.range;
|
|
258
|
+
next.active = true;
|
|
259
|
+
next.decorationId = prev.decorationId;
|
|
260
|
+
next.query = match.query;
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
next.active = false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (next.active) {
|
|
267
|
+
next.items = items(next.query!);
|
|
268
|
+
if (next.items.length) {
|
|
269
|
+
next.notFoundCount = 0;
|
|
270
|
+
} else {
|
|
271
|
+
// Update the "notFoundCount",
|
|
272
|
+
// which indicates how many characters have been typed after showing no results
|
|
273
|
+
if (next.range.to > prev.range.to) {
|
|
274
|
+
// Text has been entered (selection moved to right), but still no items found, update Count
|
|
275
|
+
next.notFoundCount = prev.notFoundCount + 1;
|
|
276
|
+
} else {
|
|
277
|
+
// No text has been entered in this tr, keep not found count
|
|
278
|
+
// (e.g.: user hits backspace after no results)
|
|
279
|
+
next.notFoundCount = prev.notFoundCount;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (next.notFoundCount > 3) {
|
|
284
|
+
next.active = false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Make sure to empty the range if suggestion is inactive
|
|
289
|
+
if (!next.active) {
|
|
290
|
+
next.decorationId = null;
|
|
291
|
+
next.range = {};
|
|
292
|
+
next.query = null;
|
|
293
|
+
next.notFoundCount = 0;
|
|
294
|
+
next.items = [];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return next;
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
props: {
|
|
302
|
+
handleKeyDown(view, event) {
|
|
303
|
+
const { active } = (this as Plugin).getState(view.state);
|
|
304
|
+
|
|
305
|
+
if (!active) {
|
|
306
|
+
// activate the popup on 'char' keypress (e.g. '/')
|
|
307
|
+
if (event.key === char) {
|
|
308
|
+
view.dispatch(
|
|
309
|
+
view.state.tr
|
|
310
|
+
.insertText(char)
|
|
311
|
+
.scrollIntoView()
|
|
312
|
+
.setMeta(pluginKey, { activate: true, type: "slash" })
|
|
313
|
+
);
|
|
314
|
+
// return true to cancel the original event, as we insert / ourselves
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// pass the key event onto the renderer (to handle arrow keys, enter and escape)
|
|
321
|
+
// return true if the event got handled by the renderer or false otherwise
|
|
322
|
+
return renderer.onKeyDown?.(event) || false;
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
// Setup decorator on the currently active suggestion.
|
|
326
|
+
decorations(state) {
|
|
327
|
+
const { active, range, decorationId, type } = (this as Plugin).getState(
|
|
328
|
+
state
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
if (!active) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// If type in meta is drag, create decoration node that wraps block
|
|
336
|
+
// Because the block does not have content yet (slash menu has the '/' in its content),
|
|
337
|
+
// so we can't use an inline decoration.
|
|
338
|
+
if (type === "drag") {
|
|
339
|
+
const blockNode = findBlock(state.selection);
|
|
340
|
+
if (blockNode) {
|
|
341
|
+
return DecorationSet.create(state.doc, [
|
|
342
|
+
Decoration.node(
|
|
343
|
+
blockNode.pos,
|
|
344
|
+
blockNode.pos + blockNode.node.nodeSize,
|
|
345
|
+
{
|
|
346
|
+
nodeName: "span",
|
|
347
|
+
class: "suggestion-decorator",
|
|
348
|
+
"data-decoration-id": decorationId,
|
|
349
|
+
}
|
|
350
|
+
),
|
|
351
|
+
]);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Create inline decoration that wraps / or whatever the specified character is
|
|
355
|
+
return DecorationSet.create(state.doc, [
|
|
356
|
+
Decoration.inline(range.from, range.to, {
|
|
357
|
+
nodeName: "span",
|
|
358
|
+
class: "suggestion-decorator",
|
|
359
|
+
"data-decoration-id": decorationId,
|
|
360
|
+
}),
|
|
361
|
+
]);
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
.suggestionWrapper {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-flow: row nowrap;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
white-space: initial;
|
|
6
|
+
padding: 0px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.buttonName {
|
|
10
|
+
font-size: small;
|
|
11
|
+
margin-bottom: 4px;
|
|
12
|
+
}
|
|
13
|
+
.buttonHint {
|
|
14
|
+
font-size: smaller;
|
|
15
|
+
color: rgb(128, 128, 128);
|
|
16
|
+
}
|
|
17
|
+
.buttonShortcut {
|
|
18
|
+
font-size: x-small;
|
|
19
|
+
color: rgb(128, 128, 128);
|
|
20
|
+
background-color: rgba(128, 128, 128, 0.2);
|
|
21
|
+
vertical-align: top;
|
|
22
|
+
padding: 3px;
|
|
23
|
+
border-radius: 3px;
|
|
24
|
+
}
|
|
25
|
+
.iconWrapper {
|
|
26
|
+
border: 1px solid rgba(128, 128, 128, 0.5);
|
|
27
|
+
border-radius: 4px;
|
|
28
|
+
background-color: white;
|
|
29
|
+
width: 40px;
|
|
30
|
+
height: 40px;
|
|
31
|
+
padding: 10px;
|
|
32
|
+
}
|
|
33
|
+
.icon {
|
|
34
|
+
width: 20px;
|
|
35
|
+
height: 20px;
|
|
36
|
+
fill: var(--N800);
|
|
37
|
+
}
|
|
38
|
+
.icon path[fill="none"] {
|
|
39
|
+
stroke: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.selectedIcon path {
|
|
43
|
+
stroke: var(--N800);
|
|
44
|
+
stroke-width: 1px;
|
|
45
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { ButtonItem, Section } from "@atlaskit/menu";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import SuggestionItem from "../SuggestionItem";
|
|
4
|
+
import styles from "./SuggestionGroup.module.css";
|
|
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
|
+
}
|
|
92
|
+
|
|
93
|
+
type SuggestionGroupProps<T> = {
|
|
94
|
+
/**
|
|
95
|
+
* Name of the group
|
|
96
|
+
*/
|
|
97
|
+
name: string;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The list of items
|
|
101
|
+
*/
|
|
102
|
+
items: T[];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Index of the selected item in this group; relative to this item group (so 0 refers to the first item in this group)
|
|
106
|
+
* This should be 'undefined' if none of the items in this group are selected
|
|
107
|
+
*/
|
|
108
|
+
selectedIndex?: number;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Callback that gets executed when an item is clicked on.
|
|
112
|
+
*/
|
|
113
|
+
clickItem: (item: T) => void;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export function SuggestionGroup<T extends SuggestionItem>(
|
|
117
|
+
props: SuggestionGroupProps<T>
|
|
118
|
+
) {
|
|
119
|
+
return (
|
|
120
|
+
<Section title={props.name}>
|
|
121
|
+
{props.items.map((item, index) => {
|
|
122
|
+
return (
|
|
123
|
+
<SuggestionComponent
|
|
124
|
+
item={item}
|
|
125
|
+
key={index} // TODO: using index as key is not ideal for performance, better have ids on suggestionItems
|
|
126
|
+
index={index}
|
|
127
|
+
selectedIndex={props.selectedIndex}
|
|
128
|
+
clickItem={props.clickItem}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
</Section>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { MenuGroup, Section } from "@atlaskit/menu";
|
|
2
|
+
import styles from "./SuggestionList.module.css";
|
|
3
|
+
import rootStyles from "../../../../root.module.css";
|
|
4
|
+
import { SuggestionGroup } from "./SuggestionGroup";
|
|
5
|
+
import SuggestionItem from "../SuggestionItem";
|
|
6
|
+
|
|
7
|
+
export type SuggestionListProps<T> = {
|
|
8
|
+
/**
|
|
9
|
+
* Object containing all suggestion items, grouped by their `groupName`.
|
|
10
|
+
*/
|
|
11
|
+
groups: {
|
|
12
|
+
[groupName: string]: T[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The total number of suggestion-items
|
|
17
|
+
*/
|
|
18
|
+
count: number;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This callback gets executed whenever an item is clicked on
|
|
22
|
+
*/
|
|
23
|
+
onSelectItem: (item: T) => void;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The index of the item that is currently selected (but not yet clicked on)
|
|
27
|
+
*/
|
|
28
|
+
selectedIndex: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Stateless component that renders the suggestion list
|
|
33
|
+
*/
|
|
34
|
+
export function SuggestionList<T extends SuggestionItem>(
|
|
35
|
+
props: SuggestionListProps<T>
|
|
36
|
+
) {
|
|
37
|
+
const renderedGroups = [];
|
|
38
|
+
|
|
39
|
+
let currentGroupIndex = 0;
|
|
40
|
+
|
|
41
|
+
for (const groupName in props.groups) {
|
|
42
|
+
const items = props.groups[groupName];
|
|
43
|
+
|
|
44
|
+
renderedGroups.push(
|
|
45
|
+
<SuggestionGroup
|
|
46
|
+
key={groupName}
|
|
47
|
+
name={groupName}
|
|
48
|
+
items={items}
|
|
49
|
+
selectedIndex={
|
|
50
|
+
props.selectedIndex >= currentGroupIndex
|
|
51
|
+
? props.selectedIndex - currentGroupIndex
|
|
52
|
+
: undefined
|
|
53
|
+
}
|
|
54
|
+
clickItem={props.onSelectItem}></SuggestionGroup>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
currentGroupIndex += items.length;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className={styles.menuList + " " + rootStyles.bnRoot}>
|
|
62
|
+
<MenuGroup>
|
|
63
|
+
{renderedGroups.length > 0 ? (
|
|
64
|
+
renderedGroups
|
|
65
|
+
) : (
|
|
66
|
+
<Section title={"No match found"}> </Section>
|
|
67
|
+
)}
|
|
68
|
+
</MenuGroup>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
// doesn't work well yet, maybe https://github.com/atomiks/tippyjs-react/issues/173
|
|
72
|
+
// We now render the tippy element manually in SuggestionListReactRenderer
|
|
73
|
+
// <Tippy
|
|
74
|
+
// visible={true}
|
|
75
|
+
// placement="bottom-start"
|
|
76
|
+
// content={
|
|
77
|
+
// <div className={styles.menuList}>
|
|
78
|
+
// <PopupMenuGroup maxWidth="250px" maxHeight="400px">
|
|
79
|
+
// {renderedGroups.length > 0 ? (
|
|
80
|
+
// renderedGroups
|
|
81
|
+
// ) : (
|
|
82
|
+
// <Section title={"No match found"}> </Section>
|
|
83
|
+
// )}
|
|
84
|
+
// </PopupMenuGroup>
|
|
85
|
+
// </div>
|
|
86
|
+
// }
|
|
87
|
+
// interactive={false}>
|
|
88
|
+
// <div></div>
|
|
89
|
+
// </Tippy>
|
|
90
|
+
);
|
|
91
|
+
}
|
package/src/style.css
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This is an empty placeholder file, which should NOT contain CSS code.
|
|
3
|
+
It's here so DEV environment doesn't show a 404
|
|
4
|
+
|
|
5
|
+
- In DEV environment, examples/editor loads this stub file, but actual CSS is loaded from CSS modules directly
|
|
6
|
+
- In PROD environment, the actual dist/style.css file is built from the CSS modules
|
|
7
|
+
*/
|
package/src/useEditor.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { EditorOptions, useEditor as useEditorTiptap } from "@tiptap/react";
|
|
2
|
+
|
|
3
|
+
import { DependencyList } from "react";
|
|
4
|
+
import { getBlockNoteExtensions } from "./BlockNoteExtensions";
|
|
5
|
+
import styles from "./editor.module.css";
|
|
6
|
+
import rootStyles from "./root.module.css";
|
|
7
|
+
|
|
8
|
+
type BlockNoteEditorOptions = EditorOptions & {
|
|
9
|
+
enableBlockNoteExtensions: boolean;
|
|
10
|
+
disableHistoryExtension: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const blockNoteExtensions = getBlockNoteExtensions();
|
|
14
|
+
|
|
15
|
+
const blockNoteOptions = {
|
|
16
|
+
enableInputRules: true,
|
|
17
|
+
enablePasteRules: true,
|
|
18
|
+
enableCoreExtensions: false,
|
|
19
|
+
};
|
|
20
|
+
export const useEditor = (
|
|
21
|
+
options: Partial<BlockNoteEditorOptions> = {},
|
|
22
|
+
deps: DependencyList = []
|
|
23
|
+
) => {
|
|
24
|
+
const extensions = options.disableHistoryExtension
|
|
25
|
+
? blockNoteExtensions.filter((e) => e.name !== "history")
|
|
26
|
+
: blockNoteExtensions;
|
|
27
|
+
|
|
28
|
+
const tiptapOptions = {
|
|
29
|
+
...blockNoteOptions,
|
|
30
|
+
...options,
|
|
31
|
+
extensions:
|
|
32
|
+
options.enableBlockNoteExtensions === false
|
|
33
|
+
? options.extensions
|
|
34
|
+
: [...(options.extensions || []), ...extensions],
|
|
35
|
+
editorProps: {
|
|
36
|
+
attributes: {
|
|
37
|
+
...(options.editorProps?.attributes || {}),
|
|
38
|
+
class: [
|
|
39
|
+
styles.bnEditor,
|
|
40
|
+
rootStyles.bnRoot,
|
|
41
|
+
(options.editorProps?.attributes as any)?.class || "",
|
|
42
|
+
].join(" "),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
return useEditorTiptap(tiptapOptions, deps);
|
|
47
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|