@blocknote/core 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocknote.js +2315 -1711
- package/dist/blocknote.js.map +1 -1
- package/dist/blocknote.umd.cjs +7 -7
- package/dist/blocknote.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +2 -2
- package/src/api/clipboard/__snapshots__/internal/basicBlocks.html +1 -0
- package/src/api/clipboard/__snapshots__/internal/basicBlocksWithProps.html +1 -0
- package/src/api/clipboard/clipboardInternal.test.ts +126 -0
- package/src/api/exporters/html/__snapshots__/pageBreak/basic/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/pageBreak/basic/internal.html +1 -0
- package/src/api/exporters/markdown/__snapshots__/pageBreak/basic/markdown.md +0 -0
- package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +16 -0
- package/src/api/parsers/html/__snapshots__/parse-codeblocks.json +62 -0
- package/src/api/parsers/html/parseHTML.test.ts +9 -0
- package/src/api/testUtil/cases/defaultSchema.ts +15 -1
- package/src/blocks/CodeBlockContent/CodeBlockContent.ts +32 -11
- package/src/blocks/HeadingBlockContent/HeadingBlockContent.ts +0 -9
- package/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +1 -1
- package/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts +1 -1
- package/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +1 -1
- package/src/blocks/PageBreakBlockContent/PageBreakBlockContent.ts +49 -0
- package/src/blocks/PageBreakBlockContent/getPageBreakSlashMenuItems.ts +45 -0
- package/src/blocks/PageBreakBlockContent/schema.ts +40 -0
- package/src/editor/Block.css +15 -1
- package/src/editor/BlockNoteEditor.ts +17 -0
- package/src/editor/BlockNoteExtensions.ts +111 -16
- package/src/editor/editor.css +22 -7
- package/src/extensions/SideMenu/SideMenuPlugin.ts +115 -23
- package/src/extensions/SideMenu/dragging.ts +0 -1
- package/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts +1 -1
- package/src/i18n/locales/ar.ts +6 -0
- package/src/i18n/locales/de.ts +6 -0
- package/src/i18n/locales/en.ts +6 -0
- package/src/i18n/locales/es.ts +6 -0
- package/src/i18n/locales/fr.ts +47 -17
- package/src/i18n/locales/hr.ts +72 -54
- package/src/i18n/locales/index.ts +1 -0
- package/src/i18n/locales/is.ts +6 -0
- package/src/i18n/locales/it.ts +315 -0
- package/src/i18n/locales/ja.ts +6 -0
- package/src/i18n/locales/ko.ts +6 -0
- package/src/i18n/locales/nl.ts +6 -0
- package/src/i18n/locales/pl.ts +6 -0
- package/src/i18n/locales/pt.ts +6 -0
- package/src/i18n/locales/ru.ts +6 -0
- package/src/i18n/locales/vi.ts +6 -0
- package/src/i18n/locales/zh.ts +6 -0
- package/src/index.ts +3 -0
- package/types/src/api/testUtil/cases/defaultSchema.d.ts +2 -1
- package/types/src/blocks/CodeBlockContent/CodeBlockContent.d.ts +2 -0
- package/types/src/blocks/PageBreakBlockContent/PageBreakBlockContent.d.ts +31 -0
- package/types/src/blocks/PageBreakBlockContent/getPageBreakSlashMenuItems.d.ts +8 -0
- package/types/src/blocks/PageBreakBlockContent/schema.d.ts +86 -0
- package/types/src/editor/BlockNoteEditor.d.ts +15 -0
- package/types/src/editor/BlockNoteExtensions.d.ts +2 -0
- package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +25 -5
- package/types/src/extensions/SuggestionMenu/DefaultSuggestionItem.d.ts +1 -1
- package/types/src/i18n/locales/de.d.ts +6 -0
- package/types/src/i18n/locales/en.d.ts +6 -0
- package/types/src/i18n/locales/es.d.ts +6 -0
- package/types/src/i18n/locales/hr.d.ts +6 -0
- package/types/src/i18n/locales/index.d.ts +1 -0
- package/types/src/i18n/locales/it.d.ts +245 -0
- package/types/src/index.d.ts +3 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { BlockNoteSchema } from "../../editor/BlockNoteSchema.js";
|
|
2
|
+
import {
|
|
3
|
+
BlockSchema,
|
|
4
|
+
InlineContentSchema,
|
|
5
|
+
StyleSchema,
|
|
6
|
+
} from "../../schema/index.js";
|
|
7
|
+
import { PageBreak } from "./PageBreakBlockContent.js";
|
|
8
|
+
|
|
9
|
+
export const pageBreakSchema = BlockNoteSchema.create({
|
|
10
|
+
blockSpecs: {
|
|
11
|
+
pageBreak: PageBreak,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Adds page break support to the given schema.
|
|
17
|
+
*/
|
|
18
|
+
export const withPageBreak = <
|
|
19
|
+
B extends BlockSchema,
|
|
20
|
+
I extends InlineContentSchema,
|
|
21
|
+
S extends StyleSchema
|
|
22
|
+
>(
|
|
23
|
+
schema: BlockNoteSchema<B, I, S>
|
|
24
|
+
) => {
|
|
25
|
+
return BlockNoteSchema.create({
|
|
26
|
+
blockSpecs: {
|
|
27
|
+
...schema.blockSpecs,
|
|
28
|
+
...pageBreakSchema.blockSpecs,
|
|
29
|
+
},
|
|
30
|
+
inlineContentSpecs: schema.inlineContentSpecs,
|
|
31
|
+
styleSpecs: schema.styleSpecs,
|
|
32
|
+
}) as any as BlockNoteSchema<
|
|
33
|
+
// typescript needs some help here
|
|
34
|
+
B & {
|
|
35
|
+
pageBreak: typeof PageBreak.config;
|
|
36
|
+
},
|
|
37
|
+
I,
|
|
38
|
+
S
|
|
39
|
+
>;
|
|
40
|
+
};
|
package/src/editor/Block.css
CHANGED
|
@@ -308,6 +308,20 @@ NESTED BLOCKS
|
|
|
308
308
|
transition-delay: 0.1s;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/* PAGE BREAK */
|
|
312
|
+
.bn-block-content[data-content-type="pageBreak"] > div {
|
|
313
|
+
width: 100%;
|
|
314
|
+
height: 0;
|
|
315
|
+
border-top: dotted rgb(125, 121, 122) 2px;
|
|
316
|
+
margin-block: 11px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
@media print {
|
|
320
|
+
.bn-block-content[data-content-type="pageBreak"] > div {
|
|
321
|
+
page-break-after: always;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
311
325
|
/* FILES */
|
|
312
326
|
|
|
313
327
|
/* Element that wraps content for all file blocks */
|
|
@@ -336,7 +350,7 @@ NESTED BLOCKS
|
|
|
336
350
|
|
|
337
351
|
.bn-editor[contenteditable="true"] [data-file-block] .bn-add-file-button:hover,
|
|
338
352
|
[data-file-block] .bn-file-name-with-icon:hover,
|
|
339
|
-
.ProseMirror-selectednode .bn-file-name-with-icon{
|
|
353
|
+
.ProseMirror-selectednode .bn-file-name-with-icon {
|
|
340
354
|
background-color: rgb(225, 225, 225);
|
|
341
355
|
}
|
|
342
356
|
|
|
@@ -196,6 +196,13 @@ export type BlockNoteEditorOptions<
|
|
|
196
196
|
* Optional function to customize how cursors of users are rendered
|
|
197
197
|
*/
|
|
198
198
|
renderCursor?: (user: any) => HTMLElement;
|
|
199
|
+
/**
|
|
200
|
+
* Optional flag to set when the user label should be shown with the default
|
|
201
|
+
* collaboration cursor. Setting to "always" will always show the label,
|
|
202
|
+
* while "activity" will only show the label when the user moves the cursor
|
|
203
|
+
* or types. Defaults to "activity".
|
|
204
|
+
*/
|
|
205
|
+
showCursorLabels?: "always" | "activity";
|
|
199
206
|
};
|
|
200
207
|
|
|
201
208
|
/**
|
|
@@ -245,6 +252,15 @@ export type BlockNoteEditorOptions<
|
|
|
245
252
|
@default "prefer-navigate-ui"
|
|
246
253
|
*/
|
|
247
254
|
tabBehavior: "prefer-navigate-ui" | "prefer-indent";
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The detection mode for showing the side menu - "viewport" always shows the
|
|
258
|
+
* side menu for the block next to the mouse cursor, while "editor" only shows
|
|
259
|
+
* it when hovering the editor or the side menu itself.
|
|
260
|
+
*
|
|
261
|
+
* @default "viewport"
|
|
262
|
+
*/
|
|
263
|
+
sideMenuDetection: "viewport" | "editor";
|
|
248
264
|
};
|
|
249
265
|
|
|
250
266
|
const blockNoteTipTapOptions = {
|
|
@@ -423,6 +439,7 @@ export class BlockNoteEditor<
|
|
|
423
439
|
dropCursor: this.options.dropCursor ?? dropCursor,
|
|
424
440
|
placeholders: newOptions.placeholders,
|
|
425
441
|
tabBehavior: newOptions.tabBehavior,
|
|
442
|
+
sideMenuDetection: newOptions.sideMenuDetection || "viewport",
|
|
426
443
|
});
|
|
427
444
|
|
|
428
445
|
// add extensions from _tiptapOptions
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AnyExtension, Extension, extensions } from "@tiptap/core";
|
|
2
|
+
import { Awareness } from "y-protocols/awareness";
|
|
2
3
|
|
|
3
4
|
import type { BlockNoteEditor, BlockNoteExtension } from "./BlockNoteEditor.js";
|
|
4
5
|
|
|
@@ -64,6 +65,7 @@ type ExtensionOptions<
|
|
|
64
65
|
};
|
|
65
66
|
provider: any;
|
|
66
67
|
renderCursor?: (user: any) => HTMLElement;
|
|
68
|
+
showCursorLabels?: "always" | "activity";
|
|
67
69
|
};
|
|
68
70
|
disableExtensions: string[] | undefined;
|
|
69
71
|
setIdAttribute?: boolean;
|
|
@@ -72,6 +74,7 @@ type ExtensionOptions<
|
|
|
72
74
|
dropCursor: (opts: any) => Plugin;
|
|
73
75
|
placeholders: Record<string | "default", string>;
|
|
74
76
|
tabBehavior?: "prefer-navigate-ui" | "prefer-indent";
|
|
77
|
+
sideMenuDetection: "viewport" | "editor";
|
|
75
78
|
};
|
|
76
79
|
|
|
77
80
|
/**
|
|
@@ -97,7 +100,10 @@ export const getBlockNoteExtensions = <
|
|
|
97
100
|
opts.editor
|
|
98
101
|
);
|
|
99
102
|
ret["linkToolbar"] = new LinkToolbarProsemirrorPlugin(opts.editor);
|
|
100
|
-
ret["sideMenu"] = new SideMenuProsemirrorPlugin(
|
|
103
|
+
ret["sideMenu"] = new SideMenuProsemirrorPlugin(
|
|
104
|
+
opts.editor,
|
|
105
|
+
opts.sideMenuDetection
|
|
106
|
+
);
|
|
101
107
|
ret["suggestionMenus"] = new SuggestionMenuProseMirrorPlugin(opts.editor);
|
|
102
108
|
ret["filePanel"] = new FilePanelProsemirrorPlugin(opts.editor as any);
|
|
103
109
|
ret["placeholder"] = new PlaceholderPlugin(opts.editor, opts.placeholders);
|
|
@@ -246,25 +252,114 @@ const getTipTapExtensions = <
|
|
|
246
252
|
fragment: opts.collaboration.fragment,
|
|
247
253
|
})
|
|
248
254
|
);
|
|
249
|
-
if (opts.collaboration.provider?.awareness) {
|
|
250
|
-
const defaultRender = (user: { color: string; name: string }) => {
|
|
251
|
-
const cursor = document.createElement("span");
|
|
252
255
|
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
const awareness = opts.collaboration?.provider.awareness as Awareness;
|
|
257
|
+
|
|
258
|
+
if (awareness) {
|
|
259
|
+
const cursors = new Map<
|
|
260
|
+
number,
|
|
261
|
+
{ element: HTMLElement; hideTimeout: NodeJS.Timeout | undefined }
|
|
262
|
+
>();
|
|
263
|
+
|
|
264
|
+
if (opts.collaboration.showCursorLabels !== "always") {
|
|
265
|
+
awareness.on(
|
|
266
|
+
"change",
|
|
267
|
+
({
|
|
268
|
+
updated,
|
|
269
|
+
}: {
|
|
270
|
+
added: Array<number>;
|
|
271
|
+
updated: Array<number>;
|
|
272
|
+
removed: Array<number>;
|
|
273
|
+
}) => {
|
|
274
|
+
for (const clientID of updated) {
|
|
275
|
+
const cursor = cursors.get(clientID);
|
|
276
|
+
|
|
277
|
+
if (cursor) {
|
|
278
|
+
cursor.element.setAttribute("data-active", "");
|
|
279
|
+
|
|
280
|
+
if (cursor.hideTimeout) {
|
|
281
|
+
clearTimeout(cursor.hideTimeout);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
cursors.set(clientID, {
|
|
285
|
+
element: cursor.element,
|
|
286
|
+
hideTimeout: setTimeout(() => {
|
|
287
|
+
cursor.element.removeAttribute("data-active");
|
|
288
|
+
}, 2000),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const createCursor = (clientID: number, name: string, color: string) => {
|
|
297
|
+
const cursorElement = document.createElement("span");
|
|
298
|
+
|
|
299
|
+
cursorElement.classList.add("collaboration-cursor__caret");
|
|
300
|
+
cursorElement.setAttribute("style", `border-color: ${color}`);
|
|
301
|
+
if (opts.collaboration?.showCursorLabels === "always") {
|
|
302
|
+
cursorElement.setAttribute("data-active", "");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const labelElement = document.createElement("span");
|
|
306
|
+
|
|
307
|
+
labelElement.classList.add("collaboration-cursor__label");
|
|
308
|
+
labelElement.setAttribute("style", `background-color: ${color}`);
|
|
309
|
+
labelElement.insertBefore(document.createTextNode(name), null);
|
|
310
|
+
|
|
311
|
+
cursorElement.insertBefore(document.createTextNode("\u2060"), null); // Non-breaking space
|
|
312
|
+
cursorElement.insertBefore(labelElement, null);
|
|
313
|
+
cursorElement.insertBefore(document.createTextNode("\u2060"), null); // Non-breaking space
|
|
314
|
+
|
|
315
|
+
cursors.set(clientID, {
|
|
316
|
+
element: cursorElement,
|
|
317
|
+
hideTimeout: undefined,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (opts.collaboration?.showCursorLabels !== "always") {
|
|
321
|
+
cursorElement.addEventListener("mouseenter", () => {
|
|
322
|
+
const cursor = cursors.get(clientID)!;
|
|
323
|
+
cursor.element.setAttribute("data-active", "");
|
|
324
|
+
|
|
325
|
+
if (cursor.hideTimeout) {
|
|
326
|
+
clearTimeout(cursor.hideTimeout);
|
|
327
|
+
cursors.set(clientID, {
|
|
328
|
+
element: cursor.element,
|
|
329
|
+
hideTimeout: undefined,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
cursorElement.addEventListener("mouseleave", () => {
|
|
335
|
+
const cursor = cursors.get(clientID)!;
|
|
336
|
+
|
|
337
|
+
cursors.set(clientID, {
|
|
338
|
+
element: cursor.element,
|
|
339
|
+
hideTimeout: setTimeout(() => {
|
|
340
|
+
cursor.element.removeAttribute("data-active");
|
|
341
|
+
}, 2000),
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return cursors.get(clientID)!;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const defaultRender = (user: { color: string; name: string }) => {
|
|
350
|
+
const clientState = [...awareness.getStates().entries()].find(
|
|
351
|
+
(state) => state[1].user === user
|
|
352
|
+
);
|
|
255
353
|
|
|
256
|
-
|
|
354
|
+
if (!clientState) {
|
|
355
|
+
throw new Error("Could not find client state for user");
|
|
356
|
+
}
|
|
257
357
|
|
|
258
|
-
|
|
259
|
-
label.setAttribute("style", `background-color: ${user.color}`);
|
|
260
|
-
label.insertBefore(document.createTextNode(user.name), null);
|
|
358
|
+
const clientID = clientState[0];
|
|
261
359
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
cursor.insertBefore(label, null);
|
|
266
|
-
cursor.insertBefore(nonbreakingSpace2, null);
|
|
267
|
-
return cursor;
|
|
360
|
+
return (
|
|
361
|
+
cursors.get(clientID) || createCursor(clientID, user.name, user.color)
|
|
362
|
+
).element;
|
|
268
363
|
};
|
|
269
364
|
tiptapExtensions.push(
|
|
270
365
|
CollaborationCursor.configure({
|
package/src/editor/editor.css
CHANGED
|
@@ -83,7 +83,6 @@ Tippy popups that are appended to document.body directly
|
|
|
83
83
|
border-right: 1px solid #0d0d0d;
|
|
84
84
|
margin-left: -1px;
|
|
85
85
|
margin-right: -1px;
|
|
86
|
-
pointer-events: none;
|
|
87
86
|
position: relative;
|
|
88
87
|
word-break: normal;
|
|
89
88
|
white-space: nowrap !important;
|
|
@@ -92,17 +91,33 @@ Tippy popups that are appended to document.body directly
|
|
|
92
91
|
/* Render the username above the caret */
|
|
93
92
|
.collaboration-cursor__label {
|
|
94
93
|
border-radius: 3px 3px 3px 0;
|
|
95
|
-
color: #0d0d0d;
|
|
96
94
|
font-size: 12px;
|
|
97
95
|
font-style: normal;
|
|
98
96
|
font-weight: 600;
|
|
99
|
-
left: -1px;
|
|
100
97
|
line-height: normal;
|
|
101
|
-
|
|
98
|
+
left: -1px;
|
|
99
|
+
overflow: hidden;
|
|
102
100
|
position: absolute;
|
|
103
|
-
top: -1.4em;
|
|
104
|
-
user-select: none;
|
|
105
101
|
white-space: nowrap;
|
|
102
|
+
|
|
103
|
+
color: transparent;
|
|
104
|
+
max-height: 4px;
|
|
105
|
+
max-width: 4px;
|
|
106
|
+
padding: 0;
|
|
107
|
+
top: 0;
|
|
108
|
+
|
|
109
|
+
transition: all 0.2s;
|
|
110
|
+
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.collaboration-cursor__caret[data-active] > .collaboration-cursor__label {
|
|
114
|
+
color: #0d0d0d;
|
|
115
|
+
max-height: 1.1rem;
|
|
116
|
+
max-width: 20rem;
|
|
117
|
+
padding: 0.1rem 0.3rem;
|
|
118
|
+
top: -14px;
|
|
119
|
+
|
|
120
|
+
transition: all 0.2s;
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
/* .tableWrapper {
|
|
@@ -134,7 +149,7 @@ Tippy popups that are appended to document.body directly
|
|
|
134
149
|
.bn-editor [data-content-type="table"] th,
|
|
135
150
|
.bn-editor [data-content-type="table"] td {
|
|
136
151
|
border: 1px solid #ddd;
|
|
137
|
-
padding:
|
|
152
|
+
padding: 5px 10px;
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
.bn-editor [data-content-type="table"] th {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { EditorState, Plugin, PluginKey } from "
|
|
3
|
-
import { EditorView } from "
|
|
1
|
+
import { DOMParser, Slice } from "@tiptap/pm/model";
|
|
2
|
+
import { EditorState, Plugin, PluginKey, PluginView } from "@tiptap/pm/state";
|
|
3
|
+
import { EditorView } from "@tiptap/pm/view";
|
|
4
4
|
|
|
5
5
|
import { Block } from "../../blocks/defaultBlocks.js";
|
|
6
6
|
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
|
|
@@ -14,6 +14,7 @@ import { EventEmitter } from "../../util/EventEmitter.js";
|
|
|
14
14
|
import { initializeESMDependencies } from "../../util/esmDependencies.js";
|
|
15
15
|
import { getDraggableBlockFromElement } from "../getDraggableBlockFromElement.js";
|
|
16
16
|
import { dragStart, unsetDragImage } from "./dragging.js";
|
|
17
|
+
|
|
17
18
|
export type SideMenuState<
|
|
18
19
|
BSchema extends BlockSchema,
|
|
19
20
|
I extends InlineContentSchema,
|
|
@@ -28,9 +29,14 @@ const PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP = 0.1;
|
|
|
28
29
|
function getBlockFromCoords(
|
|
29
30
|
view: EditorView,
|
|
30
31
|
coords: { left: number; top: number },
|
|
32
|
+
sideMenuDetection: "viewport" | "editor",
|
|
31
33
|
adjustForColumns = true
|
|
32
34
|
) {
|
|
33
|
-
const elements = view.root.elementsFromPoint(
|
|
35
|
+
const elements = view.root.elementsFromPoint(
|
|
36
|
+
// bit hacky - offset x position to right to account for the width of sidemenu itself
|
|
37
|
+
coords.left + (sideMenuDetection === "editor" ? 50 : 0),
|
|
38
|
+
coords.top
|
|
39
|
+
);
|
|
34
40
|
|
|
35
41
|
for (const element of elements) {
|
|
36
42
|
if (!view.dom.contains(element)) {
|
|
@@ -46,6 +52,7 @@ function getBlockFromCoords(
|
|
|
46
52
|
left: coords.left + 50, // bit hacky, but if we're inside a column, offset x position to right to account for the width of sidemenu itself
|
|
47
53
|
top: coords.top,
|
|
48
54
|
},
|
|
55
|
+
sideMenuDetection,
|
|
49
56
|
false
|
|
50
57
|
);
|
|
51
58
|
}
|
|
@@ -60,7 +67,8 @@ function getBlockFromMousePos(
|
|
|
60
67
|
x: number;
|
|
61
68
|
y: number;
|
|
62
69
|
},
|
|
63
|
-
view: EditorView
|
|
70
|
+
view: EditorView,
|
|
71
|
+
sideMenuDetection: "viewport" | "editor"
|
|
64
72
|
): { node: HTMLElement; id: string } | undefined {
|
|
65
73
|
// Editor itself may have padding or other styling which affects
|
|
66
74
|
// size/position, so we get the boundingRect of the first child (i.e. the
|
|
@@ -76,7 +84,7 @@ function getBlockFromMousePos(
|
|
|
76
84
|
|
|
77
85
|
// this.horizontalPosAnchor = editorBoundingBox.x;
|
|
78
86
|
|
|
79
|
-
// Gets block at mouse cursor's
|
|
87
|
+
// Gets block at mouse cursor's position.
|
|
80
88
|
const coords = {
|
|
81
89
|
left: mousePos.x,
|
|
82
90
|
top: mousePos.y,
|
|
@@ -85,15 +93,18 @@ function getBlockFromMousePos(
|
|
|
85
93
|
const mouseLeftOfEditor = coords.left < editorBoundingBox.left;
|
|
86
94
|
const mouseRightOfEditor = coords.left > editorBoundingBox.right;
|
|
87
95
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
// Clamps the x position to the editor's bounding box.
|
|
97
|
+
if (sideMenuDetection === "viewport") {
|
|
98
|
+
if (mouseLeftOfEditor) {
|
|
99
|
+
coords.left = editorBoundingBox.left + 10;
|
|
100
|
+
}
|
|
91
101
|
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
if (mouseRightOfEditor) {
|
|
103
|
+
coords.left = editorBoundingBox.right - 10;
|
|
104
|
+
}
|
|
94
105
|
}
|
|
95
106
|
|
|
96
|
-
let block = getBlockFromCoords(view, coords);
|
|
107
|
+
let block = getBlockFromCoords(view, coords, sideMenuDetection);
|
|
97
108
|
|
|
98
109
|
if (!mouseRightOfEditor && block) {
|
|
99
110
|
// note: this case is not necessary when we're on the right side of the editor
|
|
@@ -101,14 +112,14 @@ function getBlockFromMousePos(
|
|
|
101
112
|
/* Now, because blocks can be nested
|
|
102
113
|
| BlockA |
|
|
103
114
|
x | BlockB y|
|
|
104
|
-
|
|
115
|
+
|
|
105
116
|
hovering over position x (the "margin of block B") will return block A instead of block B.
|
|
106
117
|
to fix this, we get the block from the right side of block A (position y, which will fall in BlockB correctly)
|
|
107
118
|
*/
|
|
108
119
|
|
|
109
120
|
const rect = block.node.getBoundingClientRect();
|
|
110
121
|
coords.left = rect.right - 10;
|
|
111
|
-
block = getBlockFromCoords(view, coords, false);
|
|
122
|
+
block = getBlockFromCoords(view, coords, "viewport", false);
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
return block;
|
|
@@ -132,8 +143,11 @@ export class SideMenuView<
|
|
|
132
143
|
|
|
133
144
|
public menuFrozen = false;
|
|
134
145
|
|
|
146
|
+
public isDragOrigin = false;
|
|
147
|
+
|
|
135
148
|
constructor(
|
|
136
149
|
private readonly editor: BlockNoteEditor<BSchema, I, S>,
|
|
150
|
+
private readonly sideMenuDetection: "viewport" | "editor",
|
|
137
151
|
private readonly pmView: EditorView,
|
|
138
152
|
emitUpdate: (state: SideMenuState<BSchema, I, S>) => void
|
|
139
153
|
) {
|
|
@@ -146,14 +160,18 @@ export class SideMenuView<
|
|
|
146
160
|
};
|
|
147
161
|
|
|
148
162
|
this.pmView.root.addEventListener(
|
|
149
|
-
"
|
|
150
|
-
this.
|
|
151
|
-
true
|
|
163
|
+
"dragstart",
|
|
164
|
+
this.onDragStart as EventListener
|
|
152
165
|
);
|
|
153
166
|
this.pmView.root.addEventListener(
|
|
154
167
|
"dragover",
|
|
155
168
|
this.onDragOver as EventListener
|
|
156
169
|
);
|
|
170
|
+
this.pmView.root.addEventListener(
|
|
171
|
+
"drop",
|
|
172
|
+
this.onDrop as EventListener,
|
|
173
|
+
true
|
|
174
|
+
);
|
|
157
175
|
initializeESMDependencies();
|
|
158
176
|
|
|
159
177
|
// Shows or updates menu position whenever the cursor moves, if the menu isn't frozen.
|
|
@@ -181,7 +199,11 @@ export class SideMenuView<
|
|
|
181
199
|
return;
|
|
182
200
|
}
|
|
183
201
|
|
|
184
|
-
const block = getBlockFromMousePos(
|
|
202
|
+
const block = getBlockFromMousePos(
|
|
203
|
+
this.mousePos,
|
|
204
|
+
this.pmView,
|
|
205
|
+
this.sideMenuDetection
|
|
206
|
+
);
|
|
185
207
|
|
|
186
208
|
// Closes the menu if the mouse cursor is beyond the editor vertically.
|
|
187
209
|
if (!block || !this.editor.isEditable) {
|
|
@@ -249,7 +271,16 @@ export class SideMenuView<
|
|
|
249
271
|
onDrop = (event: DragEvent) => {
|
|
250
272
|
this.editor._tiptapEditor.commands.blur();
|
|
251
273
|
|
|
274
|
+
// ProseMirror doesn't remove the dragged content if it's dropped outside
|
|
275
|
+
// the editor (e.g. to other editors), so we need to do it manually. Since
|
|
276
|
+
// the dragged content is the same as the selected content, we can just
|
|
277
|
+
// delete the selection.
|
|
278
|
+
if (this.isDragOrigin && !this.pmView.dom.contains(event.target as Node)) {
|
|
279
|
+
this.pmView.dispatch(this.pmView.state.tr.deleteSelection());
|
|
280
|
+
}
|
|
281
|
+
|
|
252
282
|
if (
|
|
283
|
+
this.sideMenuDetection === "editor" ||
|
|
253
284
|
(event as any).synthetic ||
|
|
254
285
|
!event.dataTransfer?.types.includes("blocknote/html")
|
|
255
286
|
) {
|
|
@@ -268,6 +299,46 @@ export class SideMenuView<
|
|
|
268
299
|
}
|
|
269
300
|
};
|
|
270
301
|
|
|
302
|
+
/**
|
|
303
|
+
* If a block is being dragged, ProseMirror usually gets the context of what's
|
|
304
|
+
* being dragged from `view.dragging`, which is automatically set when a
|
|
305
|
+
* `dragstart` event fires in the editor. However, if the user tries to drag
|
|
306
|
+
* and drop blocks between multiple editors, only the one in which the drag
|
|
307
|
+
* began has that context, so we need to set it on the others manually. This
|
|
308
|
+
* ensures that PM always drops the blocks in between other blocks, and not
|
|
309
|
+
* inside them.
|
|
310
|
+
*
|
|
311
|
+
* After the `dragstart` event fires on the drag handle, it sets
|
|
312
|
+
* `blocknote/html` data on the clipboard. This handler fires right after,
|
|
313
|
+
* parsing the `blocknote/html` data into nodes and setting them on
|
|
314
|
+
* `view.dragging`.
|
|
315
|
+
*
|
|
316
|
+
* Note: Setting `view.dragging` on `dragover` would be better as the user
|
|
317
|
+
* could then drag between editors in different windows, but you can only
|
|
318
|
+
* access `dataTransfer` contents on `dragstart` and `drop` events.
|
|
319
|
+
*/
|
|
320
|
+
onDragStart = (event: DragEvent) => {
|
|
321
|
+
if (!this.pmView.dragging) {
|
|
322
|
+
const html = event.dataTransfer?.getData("blocknote/html");
|
|
323
|
+
if (!html) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const element = document.createElement("div");
|
|
328
|
+
element.innerHTML = html;
|
|
329
|
+
|
|
330
|
+
const parser = DOMParser.fromSchema(this.pmView.state.schema);
|
|
331
|
+
const node = parser.parse(element, {
|
|
332
|
+
topNode: this.pmView.state.schema.nodes["blockGroup"].create(),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
this.pmView.dragging = {
|
|
336
|
+
slice: new Slice(node.content, 0, 0),
|
|
337
|
+
move: true,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
271
342
|
/**
|
|
272
343
|
* If the event is outside the editor contents,
|
|
273
344
|
* we dispatch a fake event, so that we can still drop the content
|
|
@@ -275,11 +346,13 @@ export class SideMenuView<
|
|
|
275
346
|
*/
|
|
276
347
|
onDragOver = (event: DragEvent) => {
|
|
277
348
|
if (
|
|
349
|
+
this.sideMenuDetection === "editor" ||
|
|
278
350
|
(event as any).synthetic ||
|
|
279
351
|
!event.dataTransfer?.types.includes("blocknote/html")
|
|
280
352
|
) {
|
|
281
353
|
return;
|
|
282
354
|
}
|
|
355
|
+
|
|
283
356
|
const pos = this.pmView.posAtCoords({
|
|
284
357
|
left: event.clientX,
|
|
285
358
|
top: event.clientY,
|
|
@@ -424,11 +497,14 @@ export class SideMenuView<
|
|
|
424
497
|
this.onMouseMove as EventListener,
|
|
425
498
|
true
|
|
426
499
|
);
|
|
500
|
+
this.pmView.root.removeEventListener(
|
|
501
|
+
"dragstart",
|
|
502
|
+
this.onDragStart as EventListener
|
|
503
|
+
);
|
|
427
504
|
this.pmView.root.removeEventListener(
|
|
428
505
|
"dragover",
|
|
429
506
|
this.onDragOver as EventListener
|
|
430
507
|
);
|
|
431
|
-
|
|
432
508
|
this.pmView.root.removeEventListener(
|
|
433
509
|
"drop",
|
|
434
510
|
this.onDrop as EventListener,
|
|
@@ -452,14 +528,22 @@ export class SideMenuProsemirrorPlugin<
|
|
|
452
528
|
public view: SideMenuView<BSchema, I, S> | undefined;
|
|
453
529
|
public readonly plugin: Plugin;
|
|
454
530
|
|
|
455
|
-
constructor(
|
|
531
|
+
constructor(
|
|
532
|
+
private readonly editor: BlockNoteEditor<BSchema, I, S>,
|
|
533
|
+
sideMenuDetection: "viewport" | "editor"
|
|
534
|
+
) {
|
|
456
535
|
super();
|
|
457
536
|
this.plugin = new Plugin({
|
|
458
537
|
key: sideMenuPluginKey,
|
|
459
538
|
view: (editorView) => {
|
|
460
|
-
this.view = new SideMenuView(
|
|
461
|
-
|
|
462
|
-
|
|
539
|
+
this.view = new SideMenuView(
|
|
540
|
+
editor,
|
|
541
|
+
sideMenuDetection,
|
|
542
|
+
editorView,
|
|
543
|
+
(state) => {
|
|
544
|
+
this.emit("update", state);
|
|
545
|
+
}
|
|
546
|
+
);
|
|
463
547
|
return this.view;
|
|
464
548
|
},
|
|
465
549
|
});
|
|
@@ -479,6 +563,10 @@ export class SideMenuProsemirrorPlugin<
|
|
|
479
563
|
},
|
|
480
564
|
block: Block<BSchema, I, S>
|
|
481
565
|
) => {
|
|
566
|
+
if (this.view) {
|
|
567
|
+
this.view.isDragOrigin = true;
|
|
568
|
+
}
|
|
569
|
+
|
|
482
570
|
dragStart(event, block, this.editor);
|
|
483
571
|
};
|
|
484
572
|
|
|
@@ -489,6 +577,10 @@ export class SideMenuProsemirrorPlugin<
|
|
|
489
577
|
if (this.editor.prosemirrorView) {
|
|
490
578
|
unsetDragImage(this.editor.prosemirrorView.root);
|
|
491
579
|
}
|
|
580
|
+
|
|
581
|
+
if (this.view) {
|
|
582
|
+
this.view.isDragOrigin = false;
|
|
583
|
+
}
|
|
492
584
|
};
|
|
493
585
|
/**
|
|
494
586
|
* Freezes the side menu. When frozen, the side menu will stay
|
package/src/i18n/locales/ar.ts
CHANGED
|
@@ -58,6 +58,12 @@ export const ar: Dictionary = {
|
|
|
58
58
|
aliases: ["كود", "مسبق"],
|
|
59
59
|
group: "الكتل الأساسية",
|
|
60
60
|
},
|
|
61
|
+
page_break: {
|
|
62
|
+
title: "فاصل الصفحة",
|
|
63
|
+
subtext: "فاصل الصفحة",
|
|
64
|
+
aliases: ["page", "break", "separator", "فاصل", "الصفحة"],
|
|
65
|
+
group: "الكتل الأساسية",
|
|
66
|
+
},
|
|
61
67
|
table: {
|
|
62
68
|
title: "جدول",
|
|
63
69
|
subtext: "يستخدم للجداول",
|
package/src/i18n/locales/de.ts
CHANGED
|
@@ -56,6 +56,12 @@ export const de = {
|
|
|
56
56
|
aliases: ["code", "pre"],
|
|
57
57
|
group: "Grundlegende blöcke",
|
|
58
58
|
},
|
|
59
|
+
page_break: {
|
|
60
|
+
title: "Seitenumbruch",
|
|
61
|
+
subtext: "Seitentrenner",
|
|
62
|
+
aliases: ["page", "break", "separator", "seitenumbruch", "trenner"],
|
|
63
|
+
group: "Grundlegende Blöcke",
|
|
64
|
+
},
|
|
59
65
|
table: {
|
|
60
66
|
title: "Tabelle",
|
|
61
67
|
subtext: "Tabelle mit editierbaren Zellen",
|
package/src/i18n/locales/en.ts
CHANGED
|
@@ -56,6 +56,12 @@ export const en = {
|
|
|
56
56
|
aliases: ["code", "pre"],
|
|
57
57
|
group: "Basic blocks",
|
|
58
58
|
},
|
|
59
|
+
page_break: {
|
|
60
|
+
title: "Page Break",
|
|
61
|
+
subtext: "Page separator",
|
|
62
|
+
aliases: ["page", "break", "separator"],
|
|
63
|
+
group: "Basic blocks",
|
|
64
|
+
},
|
|
59
65
|
table: {
|
|
60
66
|
title: "Table",
|
|
61
67
|
subtext: "Table with editable cells",
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -55,6 +55,12 @@ export const es = {
|
|
|
55
55
|
aliases: ["code", "pre"],
|
|
56
56
|
group: "Bloques básicos",
|
|
57
57
|
},
|
|
58
|
+
page_break: {
|
|
59
|
+
title: "Salto de página",
|
|
60
|
+
subtext: "Separador de página",
|
|
61
|
+
aliases: ["page", "break", "separator", "salto", "separador"],
|
|
62
|
+
group: "Bloques básicos",
|
|
63
|
+
},
|
|
58
64
|
table: {
|
|
59
65
|
title: "Tabla",
|
|
60
66
|
subtext: "Tabla con celdas editables",
|