@blocknote/core 0.11.2 → 0.12.1
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 +13 -17
- package/dist/blocknote.js +1662 -1447
- package/dist/blocknote.js.map +1 -1
- package/dist/blocknote.umd.cjs +6 -6
- package/dist/blocknote.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +7 -3
- package/src/api/blockManipulation/blockManipulation.test.ts +19 -15
- package/src/api/blockManipulation/blockManipulation.ts +107 -17
- package/src/api/exporters/html/externalHTMLExporter.ts +3 -7
- package/src/api/exporters/html/htmlConversion.test.ts +6 -3
- package/src/api/exporters/html/internalHTMLSerializer.ts +3 -7
- package/src/api/exporters/html/util/sharedHTMLConversion.ts +3 -3
- package/src/api/exporters/markdown/markdownExporter.test.ts +7 -3
- package/src/api/exporters/markdown/markdownExporter.ts +2 -6
- package/src/api/getCurrentBlockContentType.ts +14 -0
- package/src/api/nodeConversions/nodeConversions.test.ts +14 -7
- package/src/api/nodeConversions/nodeConversions.ts +1 -2
- package/src/api/parsers/html/parseHTML.test.ts +5 -1
- package/src/api/parsers/html/parseHTML.ts +2 -6
- package/src/api/parsers/html/util/nestedLists.ts +11 -1
- package/src/api/parsers/markdown/parseMarkdown.test.ts +3 -0
- package/src/api/parsers/markdown/parseMarkdown.ts +2 -6
- package/src/api/testUtil/cases/customBlocks.ts +18 -16
- package/src/api/testUtil/cases/customInlineContent.ts +12 -13
- package/src/api/testUtil/cases/customStyles.ts +12 -10
- package/src/api/testUtil/index.ts +4 -2
- package/src/api/testUtil/partialBlockTestUtil.ts +2 -6
- package/src/blocks/HeadingBlockContent/HeadingBlockContent.ts +50 -21
- package/src/blocks/ImageBlockContent/ImageBlockContent.ts +1 -2
- package/src/blocks/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.ts +8 -1
- package/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +18 -5
- package/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts +7 -1
- package/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +18 -5
- package/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts +14 -5
- package/src/blocks/defaultBlockHelpers.ts +3 -3
- package/src/blocks/defaultBlockTypeGuards.ts +84 -0
- package/src/blocks/defaultBlocks.ts +29 -3
- package/src/editor/Block.css +2 -31
- package/src/editor/BlockNoteEditor.ts +223 -267
- package/src/editor/BlockNoteExtensions.ts +5 -2
- package/src/editor/BlockNoteSchema.ts +98 -0
- package/src/editor/BlockNoteTipTapEditor.ts +162 -0
- package/src/editor/cursorPositionTypes.ts +2 -6
- package/src/editor/editor.css +0 -1
- package/src/editor/selectionTypes.ts +2 -6
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +22 -29
- package/src/extensions/{ImageToolbar → ImagePanel}/ImageToolbarPlugin.ts +54 -60
- package/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +330 -0
- package/src/extensions/Placeholder/PlaceholderExtension.ts +81 -88
- package/src/extensions/SideMenu/SideMenuPlugin.ts +55 -56
- package/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts +8 -0
- package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +353 -0
- package/src/extensions/{SlashMenu/defaultSlashMenuItems.ts → SuggestionMenu/getDefaultSlashMenuItems.ts} +119 -89
- package/src/extensions/TableHandles/TableHandlesPlugin.ts +62 -45
- package/src/extensions-shared/UiElementPosition.ts +4 -0
- package/src/index.ts +8 -8
- package/src/pm-nodes/BlockContainer.ts +5 -5
- package/src/schema/blocks/types.ts +15 -15
- package/src/schema/inlineContent/createSpec.ts +2 -2
- package/src/schema/inlineContent/types.ts +1 -1
- package/src/util/browser.ts +6 -4
- package/src/util/typescript.ts +7 -4
- package/types/src/api/blockManipulation/blockManipulation.d.ts +6 -1
- package/types/src/api/exporters/html/externalHTMLExporter.d.ts +2 -1
- package/types/src/api/exporters/html/internalHTMLSerializer.d.ts +2 -1
- package/types/src/api/exporters/markdown/markdownExporter.d.ts +2 -1
- package/types/src/api/getCurrentBlockContentType.d.ts +2 -0
- package/types/src/api/nodeConversions/nodeConversions.d.ts +2 -1
- package/types/src/api/parsers/html/parseHTML.d.ts +2 -1
- package/types/src/api/parsers/markdown/parseMarkdown.d.ts +2 -1
- package/types/src/api/testUtil/cases/customBlocks.d.ts +72 -13
- package/types/src/api/testUtil/cases/customInlineContent.d.ts +281 -6
- package/types/src/api/testUtil/cases/customStyles.d.ts +247 -13
- package/types/src/api/testUtil/index.d.ts +4 -2
- package/types/src/api/testUtil/partialBlockTestUtil.d.ts +2 -1
- package/types/src/blocks/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.d.ts +6 -1
- package/types/src/blocks/defaultBlockHelpers.d.ts +2 -2
- package/types/src/blocks/defaultBlockTypeGuards.d.ts +24 -0
- package/types/src/blocks/defaultBlocks.d.ts +21 -15
- package/types/src/editor/BlockNoteEditor.d.ts +51 -56
- package/types/src/editor/BlockNoteExtensions.d.ts +1 -0
- package/types/src/editor/BlockNoteSchema.d.ts +34 -0
- package/types/src/editor/BlockNoteTipTapEditor.d.ts +28 -0
- package/types/src/editor/cursorPositionTypes.d.ts +2 -1
- package/types/src/editor/selectionTypes.d.ts +2 -1
- package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +5 -6
- package/types/src/extensions/ImagePanel/ImageToolbarPlugin.d.ts +32 -0
- package/types/src/extensions/LinkToolbar/LinkToolbarPlugin.d.ts +40 -0
- package/types/src/extensions/Placeholder/PlaceholderExtension.d.ts +2 -15
- package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +8 -7
- package/types/src/extensions/SuggestionMenu/DefaultSuggestionItem.d.ts +8 -0
- package/types/src/extensions/SuggestionMenu/SuggestionPlugin.d.ts +31 -0
- package/types/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.d.ts +10 -0
- package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +7 -7
- package/types/src/extensions-shared/UiElementPosition.d.ts +4 -0
- package/types/src/index.d.ts +8 -8
- package/types/src/pm-nodes/BlockContainer.d.ts +3 -2
- package/types/src/pm-nodes/BlockGroup.d.ts +1 -1
- package/types/src/schema/blocks/types.d.ts +15 -15
- package/types/src/schema/inlineContent/types.d.ts +1 -1
- package/types/src/util/browser.d.ts +1 -0
- package/types/src/util/typescript.d.ts +1 -0
- package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +0 -335
- package/src/extensions/SlashMenu/BaseSlashMenuItem.ts +0 -12
- package/src/extensions/SlashMenu/SlashMenuPlugin.ts +0 -53
- package/src/extensions-shared/BaseUiElementTypes.ts +0 -8
- package/src/extensions-shared/README.md +0 -3
- package/src/extensions-shared/suggestion/SuggestionItem.ts +0 -3
- package/src/extensions-shared/suggestion/SuggestionPlugin.ts +0 -448
- package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.d.ts +0 -38
- package/types/src/extensions/ImageToolbar/ImageToolbarPlugin.d.ts +0 -31
- package/types/src/extensions/SlashMenu/BaseSlashMenuItem.d.ts +0 -7
- package/types/src/extensions/SlashMenu/SlashMenuPlugin.d.ts +0 -13
- package/types/src/extensions/SlashMenu/defaultSlashMenuItems.d.ts +0 -3
- package/types/src/extensions-shared/BaseUiElementTypes.d.ts +0 -7
- package/types/src/extensions-shared/suggestion/SuggestionItem.d.ts +0 -3
- package/types/src/extensions-shared/suggestion/SuggestionPlugin.d.ts +0 -36
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-100.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-100.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-200.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-200.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-300.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-300.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-500.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-500.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-600.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-600.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-700.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-700.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-800.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-800.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-900.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-900.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-regular.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-regular.woff2 +0 -0
- /package/src/{assets/fonts-inter.css → fonts/inter.css} +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { getMarkRange, posToDOMRect, Range } from "@tiptap/core";
|
|
2
|
+
import { EditorView } from "@tiptap/pm/view";
|
|
3
|
+
import { Mark } from "prosemirror-model";
|
|
4
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
5
|
+
|
|
6
|
+
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
|
|
7
|
+
import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
|
|
8
|
+
import { UiElementPosition } from "../../extensions-shared/UiElementPosition";
|
|
9
|
+
import { EventEmitter } from "../../util/EventEmitter";
|
|
10
|
+
|
|
11
|
+
export type LinkToolbarState = UiElementPosition & {
|
|
12
|
+
// The hovered link's URL, and the text it's displayed with in the
|
|
13
|
+
// editor.
|
|
14
|
+
url: string;
|
|
15
|
+
text: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
class LinkToolbarView {
|
|
19
|
+
public state?: LinkToolbarState;
|
|
20
|
+
public emitUpdate: () => void;
|
|
21
|
+
|
|
22
|
+
menuUpdateTimer: ReturnType<typeof setTimeout> | undefined;
|
|
23
|
+
startMenuUpdateTimer: () => void;
|
|
24
|
+
stopMenuUpdateTimer: () => void;
|
|
25
|
+
|
|
26
|
+
mouseHoveredLinkMark: Mark | undefined;
|
|
27
|
+
mouseHoveredLinkMarkRange: Range | undefined;
|
|
28
|
+
|
|
29
|
+
keyboardHoveredLinkMark: Mark | undefined;
|
|
30
|
+
keyboardHoveredLinkMarkRange: Range | undefined;
|
|
31
|
+
|
|
32
|
+
linkMark: Mark | undefined;
|
|
33
|
+
linkMarkRange: Range | undefined;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private readonly editor: BlockNoteEditor<any, any, any>,
|
|
37
|
+
private readonly pmView: EditorView,
|
|
38
|
+
emitUpdate: (state: LinkToolbarState) => void
|
|
39
|
+
) {
|
|
40
|
+
this.emitUpdate = () => {
|
|
41
|
+
if (!this.state) {
|
|
42
|
+
throw new Error("Attempting to update uninitialized link toolbar");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
emitUpdate(this.state);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
this.startMenuUpdateTimer = () => {
|
|
49
|
+
this.menuUpdateTimer = setTimeout(() => {
|
|
50
|
+
this.update();
|
|
51
|
+
}, 250);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
this.stopMenuUpdateTimer = () => {
|
|
55
|
+
if (this.menuUpdateTimer) {
|
|
56
|
+
clearTimeout(this.menuUpdateTimer);
|
|
57
|
+
this.menuUpdateTimer = undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return false;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
this.pmView.dom.addEventListener("mouseover", this.mouseOverHandler);
|
|
64
|
+
document.addEventListener("click", this.clickHandler, true);
|
|
65
|
+
document.addEventListener("scroll", this.scrollHandler);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
mouseOverHandler = (event: MouseEvent) => {
|
|
69
|
+
// Resets the link mark currently hovered by the mouse cursor.
|
|
70
|
+
this.mouseHoveredLinkMark = undefined;
|
|
71
|
+
this.mouseHoveredLinkMarkRange = undefined;
|
|
72
|
+
|
|
73
|
+
this.stopMenuUpdateTimer();
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
event.target instanceof HTMLAnchorElement &&
|
|
77
|
+
event.target.nodeName === "A"
|
|
78
|
+
) {
|
|
79
|
+
// Finds link mark at the hovered element's position to update mouseHoveredLinkMark and
|
|
80
|
+
// mouseHoveredLinkMarkRange.
|
|
81
|
+
const hoveredLinkElement = event.target;
|
|
82
|
+
const posInHoveredLinkMark =
|
|
83
|
+
this.pmView.posAtDOM(hoveredLinkElement, 0) + 1;
|
|
84
|
+
const resolvedPosInHoveredLinkMark =
|
|
85
|
+
this.pmView.state.doc.resolve(posInHoveredLinkMark);
|
|
86
|
+
const marksAtPos = resolvedPosInHoveredLinkMark.marks();
|
|
87
|
+
|
|
88
|
+
for (const mark of marksAtPos) {
|
|
89
|
+
if (
|
|
90
|
+
mark.type.name === this.pmView.state.schema.mark("link").type.name
|
|
91
|
+
) {
|
|
92
|
+
this.mouseHoveredLinkMark = mark;
|
|
93
|
+
this.mouseHoveredLinkMarkRange =
|
|
94
|
+
getMarkRange(resolvedPosInHoveredLinkMark, mark.type, mark.attrs) ||
|
|
95
|
+
undefined;
|
|
96
|
+
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.startMenuUpdateTimer();
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
clickHandler = (event: MouseEvent) => {
|
|
108
|
+
const editorWrapper = this.pmView.dom.parentElement!;
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
// Toolbar is open.
|
|
112
|
+
this.linkMark &&
|
|
113
|
+
// An element is clicked.
|
|
114
|
+
event &&
|
|
115
|
+
event.target &&
|
|
116
|
+
// The clicked element is not the editor.
|
|
117
|
+
!(
|
|
118
|
+
editorWrapper === (event.target as Node) ||
|
|
119
|
+
editorWrapper.contains(event.target as Node)
|
|
120
|
+
)
|
|
121
|
+
) {
|
|
122
|
+
if (this.state?.show) {
|
|
123
|
+
this.state.show = false;
|
|
124
|
+
this.emitUpdate();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
scrollHandler = () => {
|
|
130
|
+
if (this.linkMark !== undefined) {
|
|
131
|
+
if (this.state?.show) {
|
|
132
|
+
this.state.referencePos = posToDOMRect(
|
|
133
|
+
this.pmView,
|
|
134
|
+
this.linkMarkRange!.from,
|
|
135
|
+
this.linkMarkRange!.to
|
|
136
|
+
);
|
|
137
|
+
this.emitUpdate();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
editLink(url: string, text: string) {
|
|
143
|
+
const tr = this.pmView.state.tr.insertText(
|
|
144
|
+
text,
|
|
145
|
+
this.linkMarkRange!.from,
|
|
146
|
+
this.linkMarkRange!.to
|
|
147
|
+
);
|
|
148
|
+
tr.addMark(
|
|
149
|
+
this.linkMarkRange!.from,
|
|
150
|
+
this.linkMarkRange!.from + text.length,
|
|
151
|
+
this.pmView.state.schema.mark("link", { href: url })
|
|
152
|
+
);
|
|
153
|
+
this.pmView.dispatch(tr);
|
|
154
|
+
this.pmView.focus();
|
|
155
|
+
|
|
156
|
+
if (this.state?.show) {
|
|
157
|
+
this.state.show = false;
|
|
158
|
+
this.emitUpdate();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
deleteLink() {
|
|
163
|
+
this.pmView.dispatch(
|
|
164
|
+
this.pmView.state.tr
|
|
165
|
+
.removeMark(
|
|
166
|
+
this.linkMarkRange!.from,
|
|
167
|
+
this.linkMarkRange!.to,
|
|
168
|
+
this.linkMark!.type
|
|
169
|
+
)
|
|
170
|
+
.setMeta("preventAutolink", true)
|
|
171
|
+
);
|
|
172
|
+
this.pmView.focus();
|
|
173
|
+
|
|
174
|
+
if (this.state?.show) {
|
|
175
|
+
this.state.show = false;
|
|
176
|
+
this.emitUpdate();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
update() {
|
|
181
|
+
if (!this.pmView.hasFocus()) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Saves the currently hovered link mark before it's updated.
|
|
186
|
+
const prevLinkMark = this.linkMark;
|
|
187
|
+
|
|
188
|
+
// Resets the currently hovered link mark.
|
|
189
|
+
this.linkMark = undefined;
|
|
190
|
+
this.linkMarkRange = undefined;
|
|
191
|
+
|
|
192
|
+
// Resets the link mark currently hovered by the keyboard cursor.
|
|
193
|
+
this.keyboardHoveredLinkMark = undefined;
|
|
194
|
+
this.keyboardHoveredLinkMarkRange = undefined;
|
|
195
|
+
|
|
196
|
+
// Finds link mark at the editor selection's position to update keyboardHoveredLinkMark and
|
|
197
|
+
// keyboardHoveredLinkMarkRange.
|
|
198
|
+
if (this.pmView.state.selection.empty) {
|
|
199
|
+
const marksAtPos = this.pmView.state.selection.$from.marks();
|
|
200
|
+
|
|
201
|
+
for (const mark of marksAtPos) {
|
|
202
|
+
if (
|
|
203
|
+
mark.type.name === this.pmView.state.schema.mark("link").type.name
|
|
204
|
+
) {
|
|
205
|
+
this.keyboardHoveredLinkMark = mark;
|
|
206
|
+
this.keyboardHoveredLinkMarkRange =
|
|
207
|
+
getMarkRange(
|
|
208
|
+
this.pmView.state.selection.$from,
|
|
209
|
+
mark.type,
|
|
210
|
+
mark.attrs
|
|
211
|
+
) || undefined;
|
|
212
|
+
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (this.mouseHoveredLinkMark) {
|
|
219
|
+
this.linkMark = this.mouseHoveredLinkMark;
|
|
220
|
+
this.linkMarkRange = this.mouseHoveredLinkMarkRange;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Keyboard cursor position takes precedence over mouse hovered link.
|
|
224
|
+
if (this.keyboardHoveredLinkMark) {
|
|
225
|
+
this.linkMark = this.keyboardHoveredLinkMark;
|
|
226
|
+
this.linkMarkRange = this.keyboardHoveredLinkMarkRange;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (this.linkMark && this.editor.isEditable) {
|
|
230
|
+
this.state = {
|
|
231
|
+
show: true,
|
|
232
|
+
referencePos: posToDOMRect(
|
|
233
|
+
this.pmView,
|
|
234
|
+
this.linkMarkRange!.from,
|
|
235
|
+
this.linkMarkRange!.to
|
|
236
|
+
),
|
|
237
|
+
url: this.linkMark!.attrs.href,
|
|
238
|
+
text: this.pmView.state.doc.textBetween(
|
|
239
|
+
this.linkMarkRange!.from,
|
|
240
|
+
this.linkMarkRange!.to
|
|
241
|
+
),
|
|
242
|
+
};
|
|
243
|
+
this.emitUpdate();
|
|
244
|
+
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Hides menu.
|
|
249
|
+
if (
|
|
250
|
+
this.state?.show &&
|
|
251
|
+
prevLinkMark &&
|
|
252
|
+
(!this.linkMark || !this.editor.isEditable)
|
|
253
|
+
) {
|
|
254
|
+
this.state.show = false;
|
|
255
|
+
this.emitUpdate();
|
|
256
|
+
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
destroy() {
|
|
262
|
+
this.pmView.dom.removeEventListener("mouseover", this.mouseOverHandler);
|
|
263
|
+
document.removeEventListener("scroll", this.scrollHandler);
|
|
264
|
+
document.removeEventListener("click", this.clickHandler, true);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export const linkToolbarPluginKey = new PluginKey("LinkToolbarPlugin");
|
|
269
|
+
|
|
270
|
+
export class LinkToolbarProsemirrorPlugin<
|
|
271
|
+
BSchema extends BlockSchema,
|
|
272
|
+
I extends InlineContentSchema,
|
|
273
|
+
S extends StyleSchema
|
|
274
|
+
> extends EventEmitter<any> {
|
|
275
|
+
private view: LinkToolbarView | undefined;
|
|
276
|
+
public readonly plugin: Plugin;
|
|
277
|
+
|
|
278
|
+
constructor(editor: BlockNoteEditor<BSchema, I, S>) {
|
|
279
|
+
super();
|
|
280
|
+
this.plugin = new Plugin({
|
|
281
|
+
key: linkToolbarPluginKey,
|
|
282
|
+
view: (editorView) => {
|
|
283
|
+
this.view = new LinkToolbarView(editor, editorView, (state) => {
|
|
284
|
+
this.emit("update", state);
|
|
285
|
+
});
|
|
286
|
+
return this.view;
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
public onUpdate(callback: (state: LinkToolbarState) => void) {
|
|
292
|
+
return this.on("update", callback);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Edit the currently hovered link.
|
|
297
|
+
*/
|
|
298
|
+
public editLink = (url: string, text: string) => {
|
|
299
|
+
this.view!.editLink(url, text);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Delete the currently hovered link.
|
|
304
|
+
*/
|
|
305
|
+
public deleteLink = () => {
|
|
306
|
+
this.view!.deleteLink();
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* When hovering on/off links using the mouse cursor, the link toolbar will
|
|
311
|
+
* open & close with a delay.
|
|
312
|
+
*
|
|
313
|
+
* This function starts the delay timer, and should be used for when the mouse
|
|
314
|
+
* cursor enters the link toolbar.
|
|
315
|
+
*/
|
|
316
|
+
public startHideTimer = () => {
|
|
317
|
+
this.view!.startMenuUpdateTimer();
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* When hovering on/off links using the mouse cursor, the link toolbar will
|
|
322
|
+
* open & close with a delay.
|
|
323
|
+
*
|
|
324
|
+
* This function stops the delay timer, and should be used for when the mouse
|
|
325
|
+
* cursor exits the link toolbar.
|
|
326
|
+
*/
|
|
327
|
+
public stopHideTimer = () => {
|
|
328
|
+
this.view!.stopMenuUpdateTimer();
|
|
329
|
+
};
|
|
330
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Node as ProsemirrorNode } from "prosemirror-model";
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
3
2
|
import { Plugin, PluginKey } from "prosemirror-state";
|
|
4
3
|
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
5
|
-
import { slashMenuPluginKey } from "../SlashMenu/SlashMenuPlugin";
|
|
6
4
|
|
|
7
5
|
const PLUGIN_KEY = new PluginKey(`blocknote-placeholder`);
|
|
8
6
|
|
|
@@ -14,21 +12,7 @@ const PLUGIN_KEY = new PluginKey(`blocknote-placeholder`);
|
|
|
14
12
|
*
|
|
15
13
|
*/
|
|
16
14
|
export interface PlaceholderOptions {
|
|
17
|
-
|
|
18
|
-
emptyNodeClass: string;
|
|
19
|
-
isFilterClass: string;
|
|
20
|
-
hasAnchorClass: string;
|
|
21
|
-
placeholder:
|
|
22
|
-
| ((PlaceholderProps: {
|
|
23
|
-
editor: Editor;
|
|
24
|
-
node: ProsemirrorNode;
|
|
25
|
-
pos: number;
|
|
26
|
-
hasAnchor: boolean;
|
|
27
|
-
}) => string)
|
|
28
|
-
| string;
|
|
29
|
-
showOnlyWhenEditable: boolean;
|
|
30
|
-
showOnlyCurrent: boolean;
|
|
31
|
-
includeChildren: boolean;
|
|
15
|
+
placeholders: Record<string | "default", string>;
|
|
32
16
|
}
|
|
33
17
|
|
|
34
18
|
export const Placeholder = Extension.create<PlaceholderOptions>({
|
|
@@ -36,93 +20,102 @@ export const Placeholder = Extension.create<PlaceholderOptions>({
|
|
|
36
20
|
|
|
37
21
|
addOptions() {
|
|
38
22
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
showOnlyCurrent: true,
|
|
46
|
-
includeChildren: false,
|
|
23
|
+
placeholders: {
|
|
24
|
+
default: "Enter text or type '/' for commands",
|
|
25
|
+
heading: "Heading",
|
|
26
|
+
bulletListItem: "List",
|
|
27
|
+
numberedListItem: "List",
|
|
28
|
+
},
|
|
47
29
|
};
|
|
48
30
|
},
|
|
49
31
|
|
|
50
32
|
addProseMirrorPlugins() {
|
|
33
|
+
const placeholders = this.options.placeholders;
|
|
51
34
|
return [
|
|
52
35
|
new Plugin({
|
|
53
36
|
key: PLUGIN_KEY,
|
|
37
|
+
view: () => {
|
|
38
|
+
const styleEl = document.createElement("style");
|
|
39
|
+
document.head.appendChild(styleEl);
|
|
40
|
+
const styleSheet = styleEl.sheet!;
|
|
41
|
+
|
|
42
|
+
const getBaseSelector = (additionalSelectors = "") =>
|
|
43
|
+
`.bn-block-content${additionalSelectors} .bn-inline-content:has(> .ProseMirror-trailingBreak):before`;
|
|
44
|
+
|
|
45
|
+
const getSelector = (
|
|
46
|
+
blockType: string | "default",
|
|
47
|
+
mustBeFocused = true
|
|
48
|
+
) => {
|
|
49
|
+
const mustBeFocusedSelector = mustBeFocused
|
|
50
|
+
? `[data-is-empty-and-focused]`
|
|
51
|
+
: ``;
|
|
52
|
+
|
|
53
|
+
if (blockType === "default") {
|
|
54
|
+
return getBaseSelector(mustBeFocusedSelector);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const blockTypeSelector = `[data-content-type="${blockType}"]`;
|
|
58
|
+
return getBaseSelector(mustBeFocusedSelector + blockTypeSelector);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
for (const [blockType, placeholder] of Object.entries(placeholders)) {
|
|
62
|
+
const mustBeFocused = blockType === "default";
|
|
63
|
+
|
|
64
|
+
styleSheet.insertRule(
|
|
65
|
+
`${getSelector(
|
|
66
|
+
blockType,
|
|
67
|
+
mustBeFocused
|
|
68
|
+
)}{ content: ${JSON.stringify(placeholder)}; }`
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// For some reason, the placeholders which show when the block is focused
|
|
72
|
+
// take priority over ones which show depending on block type, so we need
|
|
73
|
+
// to make sure the block specific ones are also used when the block is
|
|
74
|
+
// focused.
|
|
75
|
+
if (!mustBeFocused) {
|
|
76
|
+
styleSheet.insertRule(
|
|
77
|
+
`${getSelector(blockType, true)}{ content: ${JSON.stringify(
|
|
78
|
+
placeholder
|
|
79
|
+
)}; }`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
destroy: () => {
|
|
86
|
+
document.head.removeChild(styleEl);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
},
|
|
54
90
|
props: {
|
|
91
|
+
// TODO: maybe also add placeholder for empty document ("e.g.: start writing..")
|
|
55
92
|
decorations: (state) => {
|
|
56
93
|
const { doc, selection } = state;
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
const active =
|
|
60
|
-
this.editor.isEditable || !this.options.showOnlyWhenEditable;
|
|
61
|
-
const { anchor } = selection;
|
|
62
|
-
const decorations: Decoration[] = [];
|
|
94
|
+
|
|
95
|
+
const active = this.editor.isEditable;
|
|
63
96
|
|
|
64
97
|
if (!active) {
|
|
65
98
|
return;
|
|
66
99
|
}
|
|
67
100
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// If slash menu is of drag type and active, show the filter placeholder
|
|
85
|
-
if (menuState?.triggerCharacter === "" && menuState?.active) {
|
|
86
|
-
classes.push(this.options.isFilterClass);
|
|
87
|
-
}
|
|
88
|
-
// using widget, didn't work (caret position bug)
|
|
89
|
-
// const decoration = Decoration.widget(
|
|
90
|
-
// pos + 1,
|
|
91
|
-
// () => {
|
|
92
|
-
// const el = document.createElement("span");
|
|
93
|
-
// el.innerText = "hello";
|
|
94
|
-
// return el;
|
|
95
|
-
// },
|
|
96
|
-
// { side: 0 }
|
|
97
|
-
|
|
98
|
-
// Code that sets variables / classes
|
|
99
|
-
// const ph =
|
|
100
|
-
// typeof this.options.placeholder === "function"
|
|
101
|
-
// ? this.options.placeholder({
|
|
102
|
-
// editor: this.editor,
|
|
103
|
-
// node,
|
|
104
|
-
// pos,
|
|
105
|
-
// hasAnchor,
|
|
106
|
-
// })
|
|
107
|
-
// : this.options.placeholder;
|
|
108
|
-
// const decoration = Decoration.node(pos, pos + node.nodeSize, {
|
|
109
|
-
// class: classes.join(" "),
|
|
110
|
-
// style: `--placeholder:'${ph.replaceAll("'", "\\'")}';`,
|
|
111
|
-
// "data-placeholder": ph,
|
|
112
|
-
// });
|
|
113
|
-
|
|
114
|
-
// Latest version, only set isEmpty and hasAnchor, rest is done via CSS
|
|
115
|
-
|
|
116
|
-
const decoration = Decoration.node(pos, pos + node.nodeSize, {
|
|
117
|
-
class: classes.join(" "),
|
|
118
|
-
});
|
|
119
|
-
decorations.push(decoration);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return this.options.includeChildren;
|
|
101
|
+
if (!selection.empty) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const $pos = selection.$anchor;
|
|
106
|
+
const node = $pos.parent;
|
|
107
|
+
|
|
108
|
+
if (node.content.size > 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const before = $pos.before();
|
|
113
|
+
|
|
114
|
+
const dec = Decoration.node(before, before + node.nodeSize, {
|
|
115
|
+
"data-is-empty-and-focused": "true",
|
|
123
116
|
});
|
|
124
117
|
|
|
125
|
-
return DecorationSet.create(doc,
|
|
118
|
+
return DecorationSet.create(doc, [dec]);
|
|
126
119
|
},
|
|
127
120
|
},
|
|
128
121
|
}),
|