@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,390 @@
|
|
|
1
|
+
import { mergeAttributes, Node } from "@tiptap/core";
|
|
2
|
+
import { Selection, TextSelection } from "prosemirror-state";
|
|
3
|
+
import { joinBackward } from "../commands/joinBackward";
|
|
4
|
+
import { findBlock } from "../helpers/findBlock";
|
|
5
|
+
import { setBlockHeading } from "../helpers/setBlockHeading";
|
|
6
|
+
import { OrderedListPlugin } from "../OrderedListPlugin";
|
|
7
|
+
import { PreviousBlockTypePlugin } from "../PreviousBlockTypePlugin";
|
|
8
|
+
import { textblockTypeInputRuleSameNodeType } from "../rule";
|
|
9
|
+
import styles from "./Block.module.css";
|
|
10
|
+
|
|
11
|
+
export interface IBlock {
|
|
12
|
+
HTMLAttributes: Record<string, any>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type Level = 1 | 2 | 3;
|
|
16
|
+
export type ListType = "li" | "oli";
|
|
17
|
+
|
|
18
|
+
declare module "@tiptap/core" {
|
|
19
|
+
interface Commands<ReturnType> {
|
|
20
|
+
blockHeading: {
|
|
21
|
+
/**
|
|
22
|
+
* Set a heading node
|
|
23
|
+
*/
|
|
24
|
+
setBlockHeading: (attributes: { level: Level }) => ReturnType;
|
|
25
|
+
/**
|
|
26
|
+
* Unset a heading node
|
|
27
|
+
*/
|
|
28
|
+
unsetBlockHeading: () => ReturnType;
|
|
29
|
+
|
|
30
|
+
unsetList: () => ReturnType;
|
|
31
|
+
|
|
32
|
+
addNewBlockAsSibling: (attributes?: {
|
|
33
|
+
headingType?: Level;
|
|
34
|
+
listType?: ListType;
|
|
35
|
+
}) => ReturnType;
|
|
36
|
+
|
|
37
|
+
setBlockList: (type: ListType) => ReturnType;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The main "Block node" documents consist of
|
|
44
|
+
*/
|
|
45
|
+
export const Block = Node.create<IBlock>({
|
|
46
|
+
name: "tcblock",
|
|
47
|
+
group: "block",
|
|
48
|
+
addOptions() {
|
|
49
|
+
return {
|
|
50
|
+
HTMLAttributes: {},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// A block always contains content, and optionally a blockGroup which contains nested blocks
|
|
55
|
+
content: "tccontent blockgroup?",
|
|
56
|
+
|
|
57
|
+
defining: true,
|
|
58
|
+
|
|
59
|
+
addAttributes() {
|
|
60
|
+
return {
|
|
61
|
+
listType: {
|
|
62
|
+
default: undefined,
|
|
63
|
+
renderHTML: (attributes) => {
|
|
64
|
+
return {
|
|
65
|
+
"data-listType": attributes.listType,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
parseHTML: (element) => element.getAttribute("data-listType"),
|
|
69
|
+
},
|
|
70
|
+
blockColor: {
|
|
71
|
+
default: undefined,
|
|
72
|
+
renderHTML: (attributes) => {
|
|
73
|
+
return {
|
|
74
|
+
"data-blockColor": attributes.blockColor,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
parseHTML: (element) => element.getAttribute("data-blockColor"),
|
|
78
|
+
},
|
|
79
|
+
blockStyle: {
|
|
80
|
+
default: undefined,
|
|
81
|
+
renderHTML: (attributes) => {
|
|
82
|
+
return {
|
|
83
|
+
"data-blockStyle": attributes.blockStyle,
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
parseHTML: (element) => element.getAttribute("data-blockStyle"),
|
|
87
|
+
},
|
|
88
|
+
headingType: {
|
|
89
|
+
default: undefined,
|
|
90
|
+
keepOnSplit: false,
|
|
91
|
+
renderHTML: (attributes) => {
|
|
92
|
+
return {
|
|
93
|
+
"data-headingType": attributes.headingType,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
parseHTML: (element) => element.getAttribute("data-headingType"),
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// TODO: should we parse <li>, <ol>, <h1>, etc?
|
|
102
|
+
parseHTML() {
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
tag: "div",
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
renderHTML({ HTMLAttributes }) {
|
|
111
|
+
return [
|
|
112
|
+
"div",
|
|
113
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
|
114
|
+
class: styles.blockOuter,
|
|
115
|
+
}),
|
|
116
|
+
[
|
|
117
|
+
"div",
|
|
118
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
|
119
|
+
class: styles.block,
|
|
120
|
+
}),
|
|
121
|
+
0,
|
|
122
|
+
],
|
|
123
|
+
];
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
addInputRules() {
|
|
127
|
+
return [
|
|
128
|
+
...[1, 2, 3].map((level) => {
|
|
129
|
+
// Create a heading when starting with "#", "##", or "###""
|
|
130
|
+
return textblockTypeInputRuleSameNodeType({
|
|
131
|
+
find: new RegExp(`^(#{1,${level}})\\s$`),
|
|
132
|
+
type: this.type,
|
|
133
|
+
getAttributes: {
|
|
134
|
+
headingType: level,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}),
|
|
138
|
+
// Create a list when starting with "-"
|
|
139
|
+
textblockTypeInputRuleSameNodeType({
|
|
140
|
+
find: /^\s*([-+*])\s$/,
|
|
141
|
+
type: this.type,
|
|
142
|
+
getAttributes: {
|
|
143
|
+
listType: "li",
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
textblockTypeInputRuleSameNodeType({
|
|
147
|
+
find: new RegExp(/^1.\s/),
|
|
148
|
+
type: this.type,
|
|
149
|
+
getAttributes: {
|
|
150
|
+
listType: "oli",
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
];
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
addCommands() {
|
|
157
|
+
return {
|
|
158
|
+
setBlockHeading:
|
|
159
|
+
(attributes) =>
|
|
160
|
+
({ tr, dispatch }) => {
|
|
161
|
+
return setBlockHeading(tr, dispatch, attributes.level);
|
|
162
|
+
},
|
|
163
|
+
unsetBlockHeading:
|
|
164
|
+
() =>
|
|
165
|
+
({ tr, dispatch }) => {
|
|
166
|
+
return setBlockHeading(tr, dispatch, undefined);
|
|
167
|
+
},
|
|
168
|
+
unsetList:
|
|
169
|
+
() =>
|
|
170
|
+
({ tr, dispatch }) => {
|
|
171
|
+
const node = tr.selection.$anchor.node(-1);
|
|
172
|
+
const nodePos = tr.selection.$anchor.posAtIndex(0, -1) - 1;
|
|
173
|
+
|
|
174
|
+
// const node2 = tr.doc.nodeAt(nodePos);
|
|
175
|
+
if (node.type.name === "tcblock" && node.attrs["listType"]) {
|
|
176
|
+
if (dispatch) {
|
|
177
|
+
tr.setNodeMarkup(nodePos, undefined, {
|
|
178
|
+
...node.attrs,
|
|
179
|
+
listType: undefined,
|
|
180
|
+
});
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
addNewBlockAsSibling:
|
|
188
|
+
(attributes) =>
|
|
189
|
+
({ tr, dispatch, state }) => {
|
|
190
|
+
// Get current block
|
|
191
|
+
const currentBlock = findBlock(tr.selection);
|
|
192
|
+
if (!currentBlock) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// If current blocks content is empty dont create a new block
|
|
197
|
+
if (currentBlock.node.firstChild?.textContent.length === 0) {
|
|
198
|
+
if (dispatch) {
|
|
199
|
+
tr.setNodeMarkup(currentBlock.pos, undefined, attributes);
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Create new block after current block
|
|
205
|
+
const endOfBlock = currentBlock.pos + currentBlock.node.nodeSize;
|
|
206
|
+
let newBlock =
|
|
207
|
+
state.schema.nodes["tcblock"].createAndFill(attributes)!;
|
|
208
|
+
if (dispatch) {
|
|
209
|
+
tr.insert(endOfBlock, newBlock);
|
|
210
|
+
tr.setSelection(new TextSelection(tr.doc.resolve(endOfBlock + 1)));
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
},
|
|
214
|
+
setBlockList:
|
|
215
|
+
(type) =>
|
|
216
|
+
({ tr, dispatch }) => {
|
|
217
|
+
const node = tr.selection.$anchor.node(-1);
|
|
218
|
+
const nodePos = tr.selection.$anchor.posAtIndex(0, -1) - 1;
|
|
219
|
+
|
|
220
|
+
// const node2 = tr.doc.nodeAt(nodePos);
|
|
221
|
+
if (node.type.name === "tcblock") {
|
|
222
|
+
if (dispatch) {
|
|
223
|
+
tr.setNodeMarkup(nodePos, undefined, {
|
|
224
|
+
...node.attrs,
|
|
225
|
+
listType: type,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
},
|
|
232
|
+
joinBackward:
|
|
233
|
+
() =>
|
|
234
|
+
({ view, dispatch, state }) =>
|
|
235
|
+
joinBackward(state, dispatch, view), // Override default joinBackward with edited command
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
addProseMirrorPlugins() {
|
|
239
|
+
return [PreviousBlockTypePlugin(), OrderedListPlugin()];
|
|
240
|
+
},
|
|
241
|
+
addKeyboardShortcuts() {
|
|
242
|
+
// handleBackspace is partially adapted from https://github.com/ueberdosis/tiptap/blob/ed56337470efb4fd277128ab7ef792b37cfae992/packages/core/src/extensions/keymap.ts
|
|
243
|
+
const handleBackspace = () =>
|
|
244
|
+
this.editor.commands.first(({ commands }) => [
|
|
245
|
+
// Maybe the user wants to undo an auto formatting input rule (e.g.: - or #, and then hit backspace) (source: tiptap)
|
|
246
|
+
() => commands.undoInputRule(),
|
|
247
|
+
// maybe convert first text block node to default node (source: tiptap)
|
|
248
|
+
() =>
|
|
249
|
+
commands.command(({ tr }) => {
|
|
250
|
+
const { selection, doc } = tr;
|
|
251
|
+
const { empty, $anchor } = selection;
|
|
252
|
+
const { pos, parent } = $anchor;
|
|
253
|
+
const isAtStart = Selection.atStart(doc).from === pos;
|
|
254
|
+
|
|
255
|
+
if (
|
|
256
|
+
!empty ||
|
|
257
|
+
!isAtStart ||
|
|
258
|
+
!parent.type.isTextblock ||
|
|
259
|
+
parent.textContent.length
|
|
260
|
+
) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return commands.clearNodes();
|
|
265
|
+
}),
|
|
266
|
+
() => commands.deleteSelection(), // (source: tiptap)
|
|
267
|
+
() =>
|
|
268
|
+
commands.command(({ tr }) => {
|
|
269
|
+
const isAtStartOfNode = tr.selection.$anchor.parentOffset === 0;
|
|
270
|
+
const node = tr.selection.$anchor.node(-1);
|
|
271
|
+
if (isAtStartOfNode && node.type.name === "tcblock") {
|
|
272
|
+
// we're at the start of the block, so we're trying to "backspace" the bullet or indentation
|
|
273
|
+
return commands.first([
|
|
274
|
+
() => commands.unsetList(), // first try to remove the "list" property
|
|
275
|
+
() => commands.liftListItem("tcblock"), // then try to remove a level of indentation
|
|
276
|
+
]);
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
}),
|
|
280
|
+
({ chain }) =>
|
|
281
|
+
// we are at the start of a block at the root level. The user hits backspace to "merge it" to the end of the block above
|
|
282
|
+
//
|
|
283
|
+
// BlockA
|
|
284
|
+
// BlockB
|
|
285
|
+
|
|
286
|
+
// Becomes:
|
|
287
|
+
|
|
288
|
+
// BlockABlockB
|
|
289
|
+
|
|
290
|
+
chain()
|
|
291
|
+
.command(({ tr, state, dispatch }) => {
|
|
292
|
+
const isAtStartOfNode = tr.selection.$anchor.parentOffset === 0;
|
|
293
|
+
const anchor = tr.selection.$anchor;
|
|
294
|
+
const node = anchor.node(-1);
|
|
295
|
+
if (isAtStartOfNode && node.type.name === "tcblock") {
|
|
296
|
+
if (node.childCount === 2) {
|
|
297
|
+
// BlockB has children. We want to go from this:
|
|
298
|
+
//
|
|
299
|
+
// BlockA
|
|
300
|
+
// BlockB
|
|
301
|
+
// BlockC
|
|
302
|
+
// BlockD
|
|
303
|
+
//
|
|
304
|
+
// to:
|
|
305
|
+
//
|
|
306
|
+
// BlockABlockB
|
|
307
|
+
// BlockC
|
|
308
|
+
// BlockD
|
|
309
|
+
|
|
310
|
+
// This parts moves the children of BlockB to the top level
|
|
311
|
+
const startSecondChild = anchor.posAtIndex(1, -1) + 1; // start of blockgroup
|
|
312
|
+
const endSecondChild = anchor.posAtIndex(2, -1) - 1;
|
|
313
|
+
const range = state.doc
|
|
314
|
+
.resolve(startSecondChild)
|
|
315
|
+
.blockRange(state.doc.resolve(endSecondChild));
|
|
316
|
+
|
|
317
|
+
if (dispatch) {
|
|
318
|
+
tr.lift(range!, anchor.depth - 2);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
})
|
|
325
|
+
// use joinBackward to merge BlockB to BlockA (i.e.: turn it into BlockABlockB)
|
|
326
|
+
// The standard JoinBackward would break here, and would turn it into:
|
|
327
|
+
// BlockA
|
|
328
|
+
// BlockB
|
|
329
|
+
//
|
|
330
|
+
// joinBackward has been patched with our custom version to fix this (see commands/joinBackward)
|
|
331
|
+
.joinBackward()
|
|
332
|
+
.run(),
|
|
333
|
+
|
|
334
|
+
() => commands.selectNodeBackward(), // (source: tiptap)
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
const handleEnter = () =>
|
|
338
|
+
this.editor.commands.first(({ commands }) => [
|
|
339
|
+
// Try to split the current block into 2 items:
|
|
340
|
+
() => commands.splitListItem("tcblock"),
|
|
341
|
+
// Otherwise, maybe we are in an empty list item. "Enter" should remove the list bullet
|
|
342
|
+
({ tr, dispatch }) => {
|
|
343
|
+
const $from = tr.selection.$from;
|
|
344
|
+
if ($from.depth !== 3) {
|
|
345
|
+
// only needed at root level, at deeper levels it should be handled already by splitListItem
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
const node = tr.selection.$anchor.node(-1);
|
|
349
|
+
const nodePos = tr.selection.$anchor.posAtIndex(0, -1) - 1;
|
|
350
|
+
|
|
351
|
+
if (node.type.name === "tcblock" && node.attrs["listType"]) {
|
|
352
|
+
if (dispatch) {
|
|
353
|
+
tr.setNodeMarkup(nodePos, undefined, {
|
|
354
|
+
...node.attrs,
|
|
355
|
+
listType: undefined,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
},
|
|
362
|
+
// Otherwise, we might be on an empty line and hit "Enter" to create a new line:
|
|
363
|
+
({ tr, dispatch }) => {
|
|
364
|
+
const $from = tr.selection.$from;
|
|
365
|
+
|
|
366
|
+
if (dispatch) {
|
|
367
|
+
tr.split($from.pos, 2).scrollIntoView();
|
|
368
|
+
}
|
|
369
|
+
return true;
|
|
370
|
+
},
|
|
371
|
+
]);
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
Backspace: handleBackspace,
|
|
375
|
+
Enter: handleEnter,
|
|
376
|
+
Tab: () => this.editor.commands.sinkListItem("tcblock"),
|
|
377
|
+
"Shift-Tab": () => {
|
|
378
|
+
return this.editor.commands.liftListItem("tcblock");
|
|
379
|
+
},
|
|
380
|
+
"Mod-Alt-0": () =>
|
|
381
|
+
this.editor.chain().unsetList().unsetBlockHeading().run(),
|
|
382
|
+
"Mod-Alt-1": () => this.editor.commands.setBlockHeading({ level: 1 }),
|
|
383
|
+
"Mod-Alt-2": () => this.editor.commands.setBlockHeading({ level: 2 }),
|
|
384
|
+
"Mod-Alt-3": () => this.editor.commands.setBlockHeading({ level: 3 }),
|
|
385
|
+
"Mod-Shift-7": () => this.editor.commands.setBlockList("li"),
|
|
386
|
+
"Mod-Shift-8": () => this.editor.commands.setBlockList("oli"),
|
|
387
|
+
// TODO: Add shortcuts for numbered and bullet list
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { mergeAttributes, Node } from "@tiptap/core";
|
|
2
|
+
import styles from "./Block.module.css";
|
|
3
|
+
|
|
4
|
+
export const BlockGroup = Node.create({
|
|
5
|
+
name: "blockgroup",
|
|
6
|
+
|
|
7
|
+
addOptions() {
|
|
8
|
+
return {
|
|
9
|
+
HTMLAttributes: {},
|
|
10
|
+
};
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
content: "tcblock+",
|
|
14
|
+
|
|
15
|
+
parseHTML() {
|
|
16
|
+
return [{ tag: "div" }];
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
renderHTML({ HTMLAttributes }) {
|
|
20
|
+
return [
|
|
21
|
+
"div",
|
|
22
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
|
23
|
+
class: styles.blockGroup,
|
|
24
|
+
}),
|
|
25
|
+
0,
|
|
26
|
+
];
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mergeAttributes, Node } from "@tiptap/core";
|
|
2
|
+
import styles from "./Block.module.css";
|
|
3
|
+
export interface IBlock {
|
|
4
|
+
HTMLAttributes: Record<string, any>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const ContentBlock = Node.create<IBlock>({
|
|
8
|
+
name: "tccontent",
|
|
9
|
+
|
|
10
|
+
addOptions() {
|
|
11
|
+
return {
|
|
12
|
+
HTMLAttributes: {},
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
addAttributes() {
|
|
16
|
+
return {
|
|
17
|
+
position: {
|
|
18
|
+
default: undefined,
|
|
19
|
+
renderHTML: (attributes) => {
|
|
20
|
+
return {
|
|
21
|
+
"data-position": attributes.position,
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
parseHTML: (element) => element.getAttribute("data-position"),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
content: "inline*",
|
|
30
|
+
|
|
31
|
+
parseHTML() {
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
tag: "div",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
renderHTML({ HTMLAttributes }) {
|
|
40
|
+
return [
|
|
41
|
+
"div",
|
|
42
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
|
43
|
+
class: styles.blockContent,
|
|
44
|
+
}),
|
|
45
|
+
// TODO: The extra nested div is only needed for placeholders, different solution (without extra div) would be preferable
|
|
46
|
+
// We can't use the other div because the ::before attribute on that one is already reserved for list-bullets
|
|
47
|
+
["div", 0],
|
|
48
|
+
];
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Node structure
|
|
2
|
+
|
|
3
|
+
We use the following document structure:
|
|
4
|
+
|
|
5
|
+
```xml
|
|
6
|
+
<blockgroup>
|
|
7
|
+
<block>
|
|
8
|
+
<content>Parent element 1</content>
|
|
9
|
+
<blockgroup>
|
|
10
|
+
<block>
|
|
11
|
+
<content>Nested / child / indented item</content>
|
|
12
|
+
</block>
|
|
13
|
+
</blockgroup>
|
|
14
|
+
</block>
|
|
15
|
+
<block>
|
|
16
|
+
<content>Parent element 2</content>
|
|
17
|
+
<blockgroup>
|
|
18
|
+
<block>...</block>
|
|
19
|
+
<block>...</block>
|
|
20
|
+
</blockgroup>
|
|
21
|
+
</block>
|
|
22
|
+
<block>
|
|
23
|
+
<content>Element 3 without children</content>
|
|
24
|
+
</block>
|
|
25
|
+
</blockgroup>
|
|
26
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callOrReturn,
|
|
3
|
+
ExtendedRegExpMatchArray,
|
|
4
|
+
InputRule,
|
|
5
|
+
InputRuleFinder,
|
|
6
|
+
} from "@tiptap/core";
|
|
7
|
+
import { NodeType } from "prosemirror-model";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Modified version of https://github.com/ueberdosis/tiptap/blob/6a813686f5e87cebac49a624936dbeadb5a29f95/packages/core/src/inputRules/textblockTypeInputRule.ts
|
|
11
|
+
* But instead of changing the type of a node, we use setNodeMarkup to change some of it's current attributes
|
|
12
|
+
*
|
|
13
|
+
* Build an input rule that changes the type of a textblock when the
|
|
14
|
+
* matched text is typed into it. When using a regular expresion you’ll
|
|
15
|
+
* probably want the regexp to start with `^`, so that the pattern can
|
|
16
|
+
* only occur at the start of a textblock.
|
|
17
|
+
*/
|
|
18
|
+
export function textblockTypeInputRuleSameNodeType(config: {
|
|
19
|
+
find: InputRuleFinder;
|
|
20
|
+
type: NodeType;
|
|
21
|
+
getAttributes?:
|
|
22
|
+
| Record<string, any>
|
|
23
|
+
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
|
24
|
+
| false
|
|
25
|
+
| null;
|
|
26
|
+
}) {
|
|
27
|
+
return new InputRule({
|
|
28
|
+
find: config.find,
|
|
29
|
+
handler: ({ state, range, match }) => {
|
|
30
|
+
const $start = state.doc.resolve(range.from);
|
|
31
|
+
const attributes =
|
|
32
|
+
callOrReturn(config.getAttributes, undefined, match) || {};
|
|
33
|
+
|
|
34
|
+
const blockNode = $start.node(-1);
|
|
35
|
+
if (blockNode.type !== config.type) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
state.tr
|
|
40
|
+
.setNodeMarkup(range.from - 2, undefined, {
|
|
41
|
+
...blockNode.attrs,
|
|
42
|
+
...attributes,
|
|
43
|
+
})
|
|
44
|
+
.delete(range.from, range.to);
|
|
45
|
+
return;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import { PluginKey } from "prosemirror-state";
|
|
3
|
+
import ReactDOM from "react-dom";
|
|
4
|
+
import { createBubbleMenuPlugin } from "./BubbleMenuPlugin";
|
|
5
|
+
import { BubbleMenu } from "./component/BubbleMenu";
|
|
6
|
+
import rootStyles from "../../root.module.css";
|
|
7
|
+
/**
|
|
8
|
+
* The menu that is displayed when selecting a piece of text.
|
|
9
|
+
*/
|
|
10
|
+
export const BubbleMenuExtension = Extension.create<{}>({
|
|
11
|
+
name: "BubbleMenuExtension",
|
|
12
|
+
|
|
13
|
+
addProseMirrorPlugins() {
|
|
14
|
+
const element = document.createElement("div");
|
|
15
|
+
element.className = rootStyles.bnRoot;
|
|
16
|
+
ReactDOM.render(<BubbleMenu editor={this.editor} />, element);
|
|
17
|
+
return [
|
|
18
|
+
createBubbleMenuPlugin({
|
|
19
|
+
editor: this.editor,
|
|
20
|
+
element,
|
|
21
|
+
pluginKey: new PluginKey("BubbleMenuPlugin"),
|
|
22
|
+
tippyOptions: {
|
|
23
|
+
appendTo: document.body,
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
];
|
|
27
|
+
},
|
|
28
|
+
});
|