@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.
Files changed (138) hide show
  1. package/README.md +13 -17
  2. package/dist/blocknote.js +1662 -1447
  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/getCurrentBlockContentType.ts +14 -0
  18. package/src/api/nodeConversions/nodeConversions.test.ts +14 -7
  19. package/src/api/nodeConversions/nodeConversions.ts +1 -2
  20. package/src/api/parsers/html/parseHTML.test.ts +5 -1
  21. package/src/api/parsers/html/parseHTML.ts +2 -6
  22. package/src/api/parsers/html/util/nestedLists.ts +11 -1
  23. package/src/api/parsers/markdown/parseMarkdown.test.ts +3 -0
  24. package/src/api/parsers/markdown/parseMarkdown.ts +2 -6
  25. package/src/api/testUtil/cases/customBlocks.ts +18 -16
  26. package/src/api/testUtil/cases/customInlineContent.ts +12 -13
  27. package/src/api/testUtil/cases/customStyles.ts +12 -10
  28. package/src/api/testUtil/index.ts +4 -2
  29. package/src/api/testUtil/partialBlockTestUtil.ts +2 -6
  30. package/src/blocks/HeadingBlockContent/HeadingBlockContent.ts +50 -21
  31. package/src/blocks/ImageBlockContent/ImageBlockContent.ts +1 -2
  32. package/src/blocks/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.ts +8 -1
  33. package/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +18 -5
  34. package/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts +7 -1
  35. package/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +18 -5
  36. package/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts +14 -5
  37. package/src/blocks/defaultBlockHelpers.ts +3 -3
  38. package/src/blocks/defaultBlockTypeGuards.ts +84 -0
  39. package/src/blocks/defaultBlocks.ts +29 -3
  40. package/src/editor/Block.css +2 -31
  41. package/src/editor/BlockNoteEditor.ts +223 -267
  42. package/src/editor/BlockNoteExtensions.ts +5 -2
  43. package/src/editor/BlockNoteSchema.ts +98 -0
  44. package/src/editor/BlockNoteTipTapEditor.ts +162 -0
  45. package/src/editor/cursorPositionTypes.ts +2 -6
  46. package/src/editor/editor.css +0 -1
  47. package/src/editor/selectionTypes.ts +2 -6
  48. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +22 -29
  49. package/src/extensions/{ImageToolbar → ImagePanel}/ImageToolbarPlugin.ts +54 -60
  50. package/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +330 -0
  51. package/src/extensions/Placeholder/PlaceholderExtension.ts +81 -88
  52. package/src/extensions/SideMenu/SideMenuPlugin.ts +55 -56
  53. package/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts +8 -0
  54. package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +353 -0
  55. package/src/extensions/{SlashMenu/defaultSlashMenuItems.ts → SuggestionMenu/getDefaultSlashMenuItems.ts} +119 -89
  56. package/src/extensions/TableHandles/TableHandlesPlugin.ts +62 -45
  57. package/src/extensions-shared/UiElementPosition.ts +4 -0
  58. package/src/index.ts +8 -8
  59. package/src/pm-nodes/BlockContainer.ts +5 -5
  60. package/src/schema/blocks/types.ts +15 -15
  61. package/src/schema/inlineContent/createSpec.ts +2 -2
  62. package/src/schema/inlineContent/types.ts +1 -1
  63. package/src/util/browser.ts +6 -4
  64. package/src/util/typescript.ts +7 -4
  65. package/types/src/api/blockManipulation/blockManipulation.d.ts +6 -1
  66. package/types/src/api/exporters/html/externalHTMLExporter.d.ts +2 -1
  67. package/types/src/api/exporters/html/internalHTMLSerializer.d.ts +2 -1
  68. package/types/src/api/exporters/markdown/markdownExporter.d.ts +2 -1
  69. package/types/src/api/getCurrentBlockContentType.d.ts +2 -0
  70. package/types/src/api/nodeConversions/nodeConversions.d.ts +2 -1
  71. package/types/src/api/parsers/html/parseHTML.d.ts +2 -1
  72. package/types/src/api/parsers/markdown/parseMarkdown.d.ts +2 -1
  73. package/types/src/api/testUtil/cases/customBlocks.d.ts +72 -13
  74. package/types/src/api/testUtil/cases/customInlineContent.d.ts +281 -6
  75. package/types/src/api/testUtil/cases/customStyles.d.ts +247 -13
  76. package/types/src/api/testUtil/index.d.ts +4 -2
  77. package/types/src/api/testUtil/partialBlockTestUtil.d.ts +2 -1
  78. package/types/src/blocks/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.d.ts +6 -1
  79. package/types/src/blocks/defaultBlockHelpers.d.ts +2 -2
  80. package/types/src/blocks/defaultBlockTypeGuards.d.ts +24 -0
  81. package/types/src/blocks/defaultBlocks.d.ts +21 -15
  82. package/types/src/editor/BlockNoteEditor.d.ts +51 -56
  83. package/types/src/editor/BlockNoteExtensions.d.ts +1 -0
  84. package/types/src/editor/BlockNoteSchema.d.ts +34 -0
  85. package/types/src/editor/BlockNoteTipTapEditor.d.ts +28 -0
  86. package/types/src/editor/cursorPositionTypes.d.ts +2 -1
  87. package/types/src/editor/selectionTypes.d.ts +2 -1
  88. package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +5 -6
  89. package/types/src/extensions/ImagePanel/ImageToolbarPlugin.d.ts +32 -0
  90. package/types/src/extensions/LinkToolbar/LinkToolbarPlugin.d.ts +40 -0
  91. package/types/src/extensions/Placeholder/PlaceholderExtension.d.ts +2 -15
  92. package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +8 -7
  93. package/types/src/extensions/SuggestionMenu/DefaultSuggestionItem.d.ts +8 -0
  94. package/types/src/extensions/SuggestionMenu/SuggestionPlugin.d.ts +31 -0
  95. package/types/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.d.ts +10 -0
  96. package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +7 -7
  97. package/types/src/extensions-shared/UiElementPosition.d.ts +4 -0
  98. package/types/src/index.d.ts +8 -8
  99. package/types/src/pm-nodes/BlockContainer.d.ts +3 -2
  100. package/types/src/pm-nodes/BlockGroup.d.ts +1 -1
  101. package/types/src/schema/blocks/types.d.ts +15 -15
  102. package/types/src/schema/inlineContent/types.d.ts +1 -1
  103. package/types/src/util/browser.d.ts +1 -0
  104. package/types/src/util/typescript.d.ts +1 -0
  105. package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +0 -335
  106. package/src/extensions/SlashMenu/BaseSlashMenuItem.ts +0 -12
  107. package/src/extensions/SlashMenu/SlashMenuPlugin.ts +0 -53
  108. package/src/extensions-shared/BaseUiElementTypes.ts +0 -8
  109. package/src/extensions-shared/README.md +0 -3
  110. package/src/extensions-shared/suggestion/SuggestionItem.ts +0 -3
  111. package/src/extensions-shared/suggestion/SuggestionPlugin.ts +0 -448
  112. package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.d.ts +0 -38
  113. package/types/src/extensions/ImageToolbar/ImageToolbarPlugin.d.ts +0 -31
  114. package/types/src/extensions/SlashMenu/BaseSlashMenuItem.d.ts +0 -7
  115. package/types/src/extensions/SlashMenu/SlashMenuPlugin.d.ts +0 -13
  116. package/types/src/extensions/SlashMenu/defaultSlashMenuItems.d.ts +0 -3
  117. package/types/src/extensions-shared/BaseUiElementTypes.d.ts +0 -7
  118. package/types/src/extensions-shared/suggestion/SuggestionItem.d.ts +0 -3
  119. package/types/src/extensions-shared/suggestion/SuggestionPlugin.d.ts +0 -36
  120. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-100.woff +0 -0
  121. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-100.woff2 +0 -0
  122. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-200.woff +0 -0
  123. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-200.woff2 +0 -0
  124. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-300.woff +0 -0
  125. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-300.woff2 +0 -0
  126. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-500.woff +0 -0
  127. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-500.woff2 +0 -0
  128. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-600.woff +0 -0
  129. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-600.woff2 +0 -0
  130. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-700.woff +0 -0
  131. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-700.woff2 +0 -0
  132. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-800.woff +0 -0
  133. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-800.woff2 +0 -0
  134. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-900.woff +0 -0
  135. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-900.woff2 +0 -0
  136. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-regular.woff +0 -0
  137. /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-regular.woff2 +0 -0
  138. /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 { Editor, Extension } from "@tiptap/core";
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
- emptyEditorClass: string;
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
- emptyEditorClass: "bn-is-editor-empty",
40
- emptyNodeClass: "bn-is-empty",
41
- isFilterClass: "bn-is-filter",
42
- hasAnchorClass: "bn-has-anchor",
43
- placeholder: "Write something …",
44
- showOnlyWhenEditable: true,
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
- // Get state of slash menu
58
- const menuState = slashMenuPluginKey.getState(state);
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
- doc.descendants((node, pos) => {
69
- const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize;
70
- const isEmpty = !node.isLeaf && !node.childCount;
71
-
72
- if ((hasAnchor || !this.options.showOnlyCurrent) && isEmpty) {
73
- const classes = [this.options.emptyNodeClass];
74
-
75
- // TODO: Doesn't work?
76
- if (this.editor.isEmpty) {
77
- classes.push(this.options.emptyEditorClass);
78
- }
79
-
80
- if (hasAnchor) {
81
- classes.push(this.options.hasAnchorClass);
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, decorations);
118
+ return DecorationSet.create(doc, [dec]);
126
119
  },
127
120
  },
128
121
  }),