@blocknote/core 0.41.1 → 0.42.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/BlockNoteSchema-Bi-eeHal.js +3288 -0
- package/dist/BlockNoteSchema-Bi-eeHal.js.map +1 -0
- package/dist/BlockNoteSchema-DjDaA2C3.cjs +6 -0
- package/dist/BlockNoteSchema-DjDaA2C3.cjs.map +1 -0
- package/dist/blockToNode-DIfPWLH8.js +1140 -0
- package/dist/blockToNode-DIfPWLH8.js.map +1 -0
- package/dist/blockToNode-w7H99R6p.cjs +7 -0
- package/dist/blockToNode-w7H99R6p.cjs.map +1 -0
- package/dist/blocknote.cjs +4 -4
- package/dist/blocknote.cjs.map +1 -1
- package/dist/blocknote.js +1413 -1366
- package/dist/blocknote.js.map +1 -1
- package/dist/blocks.cjs +1 -1
- package/dist/blocks.js +1 -1
- package/dist/comments.cjs +1 -1
- package/dist/comments.cjs.map +1 -1
- package/dist/comments.js +49 -49
- package/dist/comments.js.map +1 -1
- package/dist/en-Cl87Uuyf.cjs.map +1 -1
- package/dist/en-njEqD7AG.js.map +1 -1
- package/dist/locales.cjs.map +1 -1
- package/dist/locales.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/webpack-stats.json +1 -1
- package/dist/yjs.cjs +2 -0
- package/dist/yjs.cjs.map +1 -0
- package/dist/yjs.js +44 -0
- package/dist/yjs.js.map +1 -0
- package/package.json +30 -25
- package/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +19 -6
- package/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.ts +173 -0
- package/src/api/nodeConversions/nodeToBlock.ts +17 -14
- package/src/blocks/Code/block.ts +1 -0
- package/src/blocks/ListItem/NumberedListItem/IndexingPlugin.ts +2 -1
- package/src/blocks/defaultBlockTypeGuards.ts +7 -18
- package/src/comments/threadstore/yjs/YjsThreadStoreBase.ts +3 -1
- package/src/editor/BlockNoteEditor.test.ts +70 -1
- package/src/editor/BlockNoteEditor.ts +55 -4
- package/src/editor/managers/ExportManager.ts +1 -1
- package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor-forked.json +2 -2
- package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor.json +2 -2
- package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-forked.html +1 -1
- package/src/extensions/Collaboration/__snapshots__/fork-yjs-snap.html +1 -1
- package/src/extensions/Collaboration/schemaMigration/SchemaMigrationPlugin.ts +10 -3
- package/src/extensions/Collaboration/schemaMigration/migrationRules/moveColorAttributes.test.ts +130 -0
- package/src/extensions/Collaboration/schemaMigration/migrationRules/moveColorAttributes.ts +34 -21
- package/src/extensions/Comments/CommentsPlugin.ts +37 -7
- package/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +30 -128
- package/src/extensions/SideMenu/SideMenuPlugin.ts +2 -0
- package/src/index.ts +1 -0
- package/src/schema/inlineContent/createSpec.ts +3 -0
- package/src/schema/inlineContent/types.ts +1 -0
- package/src/schema/schema.ts +49 -6
- package/src/schema/styles/createSpec.ts +6 -0
- package/src/schema/styles/types.ts +1 -0
- package/src/yjs/index.ts +1 -0
- package/src/yjs/utils.test.ts +1023 -0
- package/src/yjs/utils.ts +150 -0
- package/types/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.d.ts +1 -1
- package/types/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.d.ts +32 -0
- package/types/src/api/nodeConversions/nodeToBlock.d.ts +1 -1
- package/types/src/editor/BlockNoteEditor.d.ts +6 -1
- package/types/src/editor/managers/ExportManager.d.ts +1 -1
- package/types/src/extensions/Collaboration/schemaMigration/migrationRules/moveColorAttributes.test.d.ts +1 -0
- package/types/src/extensions/Comments/CommentsPlugin.d.ts +1 -1
- package/types/src/index.d.ts +1 -0
- package/types/src/schema/inlineContent/createSpec.d.ts +1 -0
- package/types/src/schema/inlineContent/types.d.ts +1 -0
- package/types/src/schema/styles/createSpec.d.ts +1 -0
- package/types/src/schema/styles/types.d.ts +1 -0
- package/types/src/yjs/index.d.ts +1 -0
- package/types/src/yjs/utils.d.ts +55 -0
- package/types/src/yjs/utils.test.d.ts +1 -0
- package/dist/BlockNoteSchema-COA0fsXW.cjs +0 -11
- package/dist/BlockNoteSchema-COA0fsXW.cjs.map +0 -1
- package/dist/BlockNoteSchema-CYRHak18.js +0 -4375
- package/dist/BlockNoteSchema-CYRHak18.js.map +0 -1
- package/src/extensions/BackgroundColor/BackgroundColorMark.ts +0 -46
- package/src/extensions/TextColor/TextColorMark.ts +0 -38
- package/types/src/extensions/BackgroundColor/BackgroundColorMark.d.ts +0 -10
- package/types/src/extensions/TextColor/TextColorMark.d.ts +0 -10
|
@@ -50,7 +50,7 @@ export function editorHasBlockWithType<
|
|
|
50
50
|
if (typeof propSpec === "string") {
|
|
51
51
|
if (
|
|
52
52
|
editor.schema.blockSpecs[blockType].config.propSchema[propName]
|
|
53
|
-
.default &&
|
|
53
|
+
.default !== undefined &&
|
|
54
54
|
typeof editor.schema.blockSpecs[blockType].config.propSchema[propName]
|
|
55
55
|
.default !== propSpec
|
|
56
56
|
) {
|
|
@@ -58,7 +58,8 @@ export function editorHasBlockWithType<
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
if (
|
|
61
|
-
editor.schema.blockSpecs[blockType].config.propSchema[propName].type
|
|
61
|
+
editor.schema.blockSpecs[blockType].config.propSchema[propName].type !==
|
|
62
|
+
undefined &&
|
|
62
63
|
editor.schema.blockSpecs[blockType].config.propSchema[propName].type !==
|
|
63
64
|
propSpec
|
|
64
65
|
) {
|
|
@@ -97,23 +98,11 @@ export function editorHasBlockWithType<
|
|
|
97
98
|
.values === "object" &&
|
|
98
99
|
typeof propSpec.values === "object"
|
|
99
100
|
) {
|
|
100
|
-
|
|
101
|
-
editor.schema.blockSpecs[blockType].config.propSchema[propName].values
|
|
102
|
-
.length !== propSpec.values.length
|
|
103
|
-
) {
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
for (
|
|
108
|
-
let i = 0;
|
|
109
|
-
i <
|
|
110
|
-
editor.schema.blockSpecs[blockType].config.propSchema[propName].values
|
|
111
|
-
.length;
|
|
112
|
-
i++
|
|
113
|
-
) {
|
|
101
|
+
for (const value of propSpec.values) {
|
|
114
102
|
if (
|
|
115
|
-
editor.schema.blockSpecs[blockType].config.propSchema[
|
|
116
|
-
|
|
103
|
+
!editor.schema.blockSpecs[blockType].config.propSchema[
|
|
104
|
+
propName
|
|
105
|
+
].values.includes(value)
|
|
117
106
|
) {
|
|
118
107
|
return false;
|
|
119
108
|
}
|
|
@@ -29,7 +29,9 @@ export abstract class YjsThreadStoreBase extends ThreadStore {
|
|
|
29
29
|
public getThreads(): Map<string, ThreadData> {
|
|
30
30
|
const threadMap = new Map<string, ThreadData>();
|
|
31
31
|
this.threadsYMap.forEach((yThread, id) => {
|
|
32
|
-
|
|
32
|
+
if (yThread instanceof Y.Map) {
|
|
33
|
+
threadMap.set(id, yMapToThread(yThread));
|
|
34
|
+
}
|
|
33
35
|
});
|
|
34
36
|
return threadMap;
|
|
35
37
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "../api/getBlockInfoFromPos.js";
|
|
6
6
|
import { BlockNoteEditor } from "./BlockNoteEditor.js";
|
|
7
7
|
import { BlockNoteExtension } from "./BlockNoteExtension.js";
|
|
8
|
+
import * as Y from "yjs";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @vitest-environment jsdom
|
|
@@ -104,7 +105,7 @@ it("block prop types", () => {
|
|
|
104
105
|
}
|
|
105
106
|
});
|
|
106
107
|
|
|
107
|
-
it("onMount and onUnmount", () => {
|
|
108
|
+
it("onMount and onUnmount", async () => {
|
|
108
109
|
const editor = BlockNoteEditor.create();
|
|
109
110
|
let mounted = false;
|
|
110
111
|
let unmounted = false;
|
|
@@ -118,6 +119,10 @@ it("onMount and onUnmount", () => {
|
|
|
118
119
|
expect(mounted).toBe(true);
|
|
119
120
|
expect(unmounted).toBe(false);
|
|
120
121
|
editor.unmount();
|
|
122
|
+
// expect the unmount event to not have been triggered yet, since it waits 2 ticks
|
|
123
|
+
expect(unmounted).toBe(false);
|
|
124
|
+
// wait 3 ticks to ensure the unmount event is triggered
|
|
125
|
+
await new Promise((resolve) => setTimeout(resolve, 3));
|
|
121
126
|
expect(mounted).toBe(true);
|
|
122
127
|
expect(unmounted).toBe(true);
|
|
123
128
|
});
|
|
@@ -142,3 +147,67 @@ it("onCreate event", () => {
|
|
|
142
147
|
});
|
|
143
148
|
expect(created).toBe(true);
|
|
144
149
|
});
|
|
150
|
+
|
|
151
|
+
it("sets an initial block id when using Y.js", async () => {
|
|
152
|
+
const doc = new Y.Doc();
|
|
153
|
+
const fragment = doc.getXmlFragment("doc");
|
|
154
|
+
let transactionCount = 0;
|
|
155
|
+
const editor = BlockNoteEditor.create({
|
|
156
|
+
collaboration: {
|
|
157
|
+
fragment,
|
|
158
|
+
user: { name: "Hello", color: "#FFFFFF" },
|
|
159
|
+
provider: null,
|
|
160
|
+
},
|
|
161
|
+
_tiptapOptions: {
|
|
162
|
+
onTransaction: () => {
|
|
163
|
+
transactionCount++;
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
editor.mount(document.createElement("div"));
|
|
169
|
+
|
|
170
|
+
expect(editor.prosemirrorState.doc.toJSON()).toMatchInlineSnapshot(`
|
|
171
|
+
{
|
|
172
|
+
"content": [
|
|
173
|
+
{
|
|
174
|
+
"content": [
|
|
175
|
+
{
|
|
176
|
+
"attrs": {
|
|
177
|
+
"id": "initialBlockId",
|
|
178
|
+
},
|
|
179
|
+
"content": [
|
|
180
|
+
{
|
|
181
|
+
"attrs": {
|
|
182
|
+
"backgroundColor": "default",
|
|
183
|
+
"textAlignment": "left",
|
|
184
|
+
"textColor": "default",
|
|
185
|
+
},
|
|
186
|
+
"type": "paragraph",
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
"type": "blockContainer",
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
"type": "blockGroup",
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
"type": "doc",
|
|
196
|
+
}
|
|
197
|
+
`);
|
|
198
|
+
expect(transactionCount).toBe(1);
|
|
199
|
+
// The fragment should not be modified yet, since the editor's content is only the initial content
|
|
200
|
+
expect(fragment.toJSON()).toMatchInlineSnapshot(`""`);
|
|
201
|
+
|
|
202
|
+
editor.replaceBlocks(editor.document, [
|
|
203
|
+
{
|
|
204
|
+
type: "paragraph",
|
|
205
|
+
content: [{ text: "Hello", styles: {}, type: "text" }],
|
|
206
|
+
},
|
|
207
|
+
]);
|
|
208
|
+
expect(transactionCount).toBe(2);
|
|
209
|
+
// Only after a real modification is made, will the fragment be updated
|
|
210
|
+
expect(fragment.toJSON()).toMatchInlineSnapshot(
|
|
211
|
+
`"<blockgroup><blockcontainer id="0"><paragraph backgroundColor="default" textAlignment="left" textColor="default">Hello</paragraph></blockcontainer><blockcontainer id="1"><paragraph backgroundColor="default" textAlignment="left" textColor="default"></paragraph></blockcontainer></blockgroup>"`,
|
|
212
|
+
);
|
|
213
|
+
});
|
|
@@ -917,6 +917,25 @@ export class BlockNoteEditor<
|
|
|
917
917
|
);
|
|
918
918
|
}
|
|
919
919
|
|
|
920
|
+
// When y-prosemirror creates an empty document, the `blockContainer` node is created with an `id` of `null`.
|
|
921
|
+
// This causes the unique id extension to generate a new id for the initial block, which is not what we want
|
|
922
|
+
// Since it will be randomly generated & cause there to be more updates to the ydoc
|
|
923
|
+
// This is a hack to make it so that anytime `schema.doc.createAndFill` is called, the initial block id is already set to "initialBlockId"
|
|
924
|
+
let cache: Node | undefined = undefined;
|
|
925
|
+
const oldCreateAndFill = this.pmSchema.nodes.doc.createAndFill;
|
|
926
|
+
this.pmSchema.nodes.doc.createAndFill = (...args: any) => {
|
|
927
|
+
if (cache) {
|
|
928
|
+
return cache;
|
|
929
|
+
}
|
|
930
|
+
const ret = oldCreateAndFill.apply(this.pmSchema.nodes.doc, args)!;
|
|
931
|
+
|
|
932
|
+
// create a copy that we can mutate (otherwise, assigning attrs is not safe and corrupts the pm state)
|
|
933
|
+
const jsonNode = JSON.parse(JSON.stringify(ret.toJSON()));
|
|
934
|
+
jsonNode.content[0].content[0].attrs.id = "initialBlockId";
|
|
935
|
+
|
|
936
|
+
cache = Node.fromJSON(this.pmSchema, jsonNode);
|
|
937
|
+
return cache;
|
|
938
|
+
};
|
|
920
939
|
this.pmSchema.cached.blockNoteEditor = this;
|
|
921
940
|
|
|
922
941
|
// Initialize managers
|
|
@@ -1035,15 +1054,40 @@ export class BlockNoteEditor<
|
|
|
1035
1054
|
* @warning Not needed to call manually when using React, use BlockNoteView to take care of mounting
|
|
1036
1055
|
*/
|
|
1037
1056
|
public mount = (element: HTMLElement) => {
|
|
1038
|
-
|
|
1039
|
-
|
|
1057
|
+
if (
|
|
1058
|
+
// If the editor is scheduled for destruction, and
|
|
1059
|
+
this.scheduledDestructionTimeout &&
|
|
1060
|
+
// If the editor is being remounted to the same element as the one which is scheduled for destruction,
|
|
1061
|
+
// then just cancel the destruction timeout
|
|
1062
|
+
this.prosemirrorView.dom === element
|
|
1063
|
+
) {
|
|
1064
|
+
clearTimeout(this.scheduledDestructionTimeout);
|
|
1065
|
+
this.scheduledDestructionTimeout = undefined;
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
this._tiptapEditor.mount({ mount: element });
|
|
1040
1070
|
};
|
|
1041
1071
|
|
|
1072
|
+
/**
|
|
1073
|
+
* Timeout to schedule the {@link unmount}ing of the editor.
|
|
1074
|
+
*/
|
|
1075
|
+
private scheduledDestructionTimeout:
|
|
1076
|
+
| ReturnType<typeof setTimeout>
|
|
1077
|
+
| undefined = undefined;
|
|
1078
|
+
|
|
1042
1079
|
/**
|
|
1043
1080
|
* Unmount the editor from the DOM element it is bound to
|
|
1044
1081
|
*/
|
|
1045
1082
|
public unmount = () => {
|
|
1046
|
-
|
|
1083
|
+
// Due to how React's StrictMode works, it will `unmount` & `mount` the component twice in development mode.
|
|
1084
|
+
// This can result in the editor being unmounted mid-rendering the content of node views.
|
|
1085
|
+
// To avoid this, we only ever schedule the `unmount`ing of the editor when we've seen whether React "meant" to actually unmount the editor (i.e. not calling mount one tick later).
|
|
1086
|
+
// So, we wait two ticks to see if the component is still meant to be unmounted, and if not, we actually unmount the editor.
|
|
1087
|
+
this.scheduledDestructionTimeout = setTimeout(() => {
|
|
1088
|
+
this._tiptapEditor.unmount();
|
|
1089
|
+
this.scheduledDestructionTimeout = undefined;
|
|
1090
|
+
}, 1);
|
|
1047
1091
|
};
|
|
1048
1092
|
|
|
1049
1093
|
/**
|
|
@@ -1082,6 +1126,13 @@ export class BlockNoteEditor<
|
|
|
1082
1126
|
this.prosemirrorView.focus();
|
|
1083
1127
|
}
|
|
1084
1128
|
|
|
1129
|
+
public blur() {
|
|
1130
|
+
if (this.headless) {
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
this.prosemirrorView.dom.blur();
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1085
1136
|
public onUploadStart(callback: (blockId?: string) => void) {
|
|
1086
1137
|
this.onUploadStartCallbacks.push(callback);
|
|
1087
1138
|
|
|
@@ -1482,7 +1533,7 @@ export class BlockNoteEditor<
|
|
|
1482
1533
|
* @returns The blocks, serialized as an HTML string.
|
|
1483
1534
|
*/
|
|
1484
1535
|
public blocksToFullHTML(
|
|
1485
|
-
blocks: PartialBlock<BSchema, ISchema, SSchema>[],
|
|
1536
|
+
blocks: PartialBlock<BSchema, ISchema, SSchema>[] = this.document,
|
|
1486
1537
|
): string {
|
|
1487
1538
|
return this._exportManager.blocksToFullHTML(blocks);
|
|
1488
1539
|
}
|
|
@@ -54,7 +54,7 @@ export class ExportManager<
|
|
|
54
54
|
* @returns The blocks, serialized as an HTML string.
|
|
55
55
|
*/
|
|
56
56
|
public blocksToFullHTML(
|
|
57
|
-
blocks: PartialBlock<BSchema, ISchema, SSchema>[],
|
|
57
|
+
blocks: PartialBlock<BSchema, ISchema, SSchema>[] = this.editor.document,
|
|
58
58
|
): string {
|
|
59
59
|
const exporter = createInternalHTMLSerializer(
|
|
60
60
|
this.editor.pmSchema,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "text",
|
|
9
9
|
},
|
|
10
10
|
],
|
|
11
|
-
"id": "
|
|
11
|
+
"id": "2",
|
|
12
12
|
"props": {
|
|
13
13
|
"backgroundColor": "default",
|
|
14
14
|
"textAlignment": "left",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
{
|
|
20
20
|
"children": [],
|
|
21
21
|
"content": [],
|
|
22
|
-
"id": "
|
|
22
|
+
"id": "3",
|
|
23
23
|
"props": {
|
|
24
24
|
"backgroundColor": "default",
|
|
25
25
|
"textAlignment": "left",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "text",
|
|
9
9
|
},
|
|
10
10
|
],
|
|
11
|
-
"id": "
|
|
11
|
+
"id": "0",
|
|
12
12
|
"props": {
|
|
13
13
|
"backgroundColor": "default",
|
|
14
14
|
"textAlignment": "left",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
{
|
|
20
20
|
"children": [],
|
|
21
21
|
"content": [],
|
|
22
|
-
"id": "
|
|
22
|
+
"id": "1",
|
|
23
23
|
"props": {
|
|
24
24
|
"backgroundColor": "default",
|
|
25
25
|
"textAlignment": "left",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<blockgroup><blockcontainer id="
|
|
1
|
+
<blockgroup><blockcontainer id="2"><paragraph backgroundColor="default" textAlignment="left" textColor="default">Hello World</paragraph></blockcontainer><blockcontainer id="3"><paragraph backgroundColor="default" textAlignment="left" textColor="default"></paragraph></blockcontainer></blockgroup>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<blockgroup><blockcontainer id="
|
|
1
|
+
<blockgroup><blockcontainer id="0"><paragraph backgroundColor="default" textAlignment="left" textColor="default">Hello</paragraph></blockcontainer><blockcontainer id="1"><paragraph backgroundColor="default" textAlignment="left" textColor="default"></paragraph></blockcontainer></blockgroup>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
2
|
-
import { ySyncPluginKey } from "y-prosemirror";
|
|
3
2
|
import * as Y from "yjs";
|
|
4
3
|
|
|
5
4
|
import { BlockNoteExtension } from "../../../editor/BlockNoteExtension.js";
|
|
@@ -31,8 +30,12 @@ export class SchemaMigrationPlugin extends BlockNoteExtension {
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
if (
|
|
34
|
-
transactions
|
|
35
|
-
!transactions
|
|
33
|
+
// If any of the transactions are not due to a yjs sync, we don't need to run the migration
|
|
34
|
+
!transactions.some((tr) => tr.getMeta("y-sync$")) ||
|
|
35
|
+
// If none of the transactions result in a document change, we don't need to run the migration
|
|
36
|
+
transactions.every((tr) => !tr.docChanged) ||
|
|
37
|
+
// If the fragment is still empty, we can't run the migration (since it has not yet been applied to the Y.Doc)
|
|
38
|
+
!fragment.firstChild
|
|
36
39
|
) {
|
|
37
40
|
return undefined;
|
|
38
41
|
}
|
|
@@ -44,6 +47,10 @@ export class SchemaMigrationPlugin extends BlockNoteExtension {
|
|
|
44
47
|
|
|
45
48
|
this.migrationDone = true;
|
|
46
49
|
|
|
50
|
+
if (!tr.docChanged) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
47
54
|
return tr;
|
|
48
55
|
},
|
|
49
56
|
}),
|
package/src/extensions/Collaboration/schemaMigration/migrationRules/moveColorAttributes.test.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { expect, it } from "vitest";
|
|
2
|
+
import * as Y from "yjs";
|
|
3
|
+
import { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
|
|
4
|
+
import { moveColorAttributes } from "./moveColorAttributes.js";
|
|
5
|
+
import { prosemirrorJSONToYXmlFragment } from "y-prosemirror";
|
|
6
|
+
|
|
7
|
+
it("can move color attributes on older documents", async () => {
|
|
8
|
+
const doc = new Y.Doc();
|
|
9
|
+
const fragment = doc.getXmlFragment("doc");
|
|
10
|
+
const editor = BlockNoteEditor.create({
|
|
11
|
+
initialContent: [
|
|
12
|
+
{
|
|
13
|
+
type: "paragraph",
|
|
14
|
+
content: "Welcome to this demo!",
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Because this was a previous schema, we are creating the YFragment manually
|
|
20
|
+
const blockGroup = new Y.XmlElement("blockGroup");
|
|
21
|
+
const el = new Y.XmlElement("blockContainer");
|
|
22
|
+
el.setAttribute("id", "0");
|
|
23
|
+
el.setAttribute("backgroundColor", "red");
|
|
24
|
+
el.setAttribute("textColor", "blue");
|
|
25
|
+
const para = new Y.XmlElement("paragraph");
|
|
26
|
+
para.setAttribute("textAlignment", "left");
|
|
27
|
+
para.insert(0, [new Y.XmlText("Welcome to this demo!")]);
|
|
28
|
+
el.insert(0, [para]);
|
|
29
|
+
blockGroup.insert(0, [el]);
|
|
30
|
+
fragment.insert(0, [blockGroup]);
|
|
31
|
+
|
|
32
|
+
// Note that the blockContainer has the color attributes, but the paragraph does not.
|
|
33
|
+
expect(fragment.toJSON()).toMatchInlineSnapshot(
|
|
34
|
+
`"<blockgroup><blockcontainer backgroundColor="red" id="0" textColor="blue"><paragraph textAlignment="left">Welcome to this demo!</paragraph></blockcontainer></blockgroup>"`,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const tr = editor.prosemirrorState.tr;
|
|
38
|
+
moveColorAttributes(fragment, tr);
|
|
39
|
+
// Note that the color attributes have been moved to the paragraph.
|
|
40
|
+
expect(JSON.stringify(tr.doc.toJSON())).toMatchInlineSnapshot(
|
|
41
|
+
`"{"type":"doc","content":[{"type":"blockGroup","content":[{"type":"blockContainer","attrs":{"id":"0"},"content":[{"type":"paragraph","attrs":{"backgroundColor":"red","textColor":"blue","textAlignment":"left"},"content":[{"type":"text","text":"Welcome to this demo!"}]}]}]}]}"`,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("does not move color attributes on newer documents", async () => {
|
|
46
|
+
const doc = new Y.Doc();
|
|
47
|
+
const fragment = doc.getXmlFragment("doc");
|
|
48
|
+
const editor = BlockNoteEditor.create({
|
|
49
|
+
initialContent: [
|
|
50
|
+
{
|
|
51
|
+
type: "paragraph",
|
|
52
|
+
content: "Welcome to this demo!",
|
|
53
|
+
props: {
|
|
54
|
+
backgroundColor: "red",
|
|
55
|
+
textColor: "blue",
|
|
56
|
+
// Set to non-default value to ensure it is not overridden by the migration rule.
|
|
57
|
+
textAlignment: "right",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
prosemirrorJSONToYXmlFragment(
|
|
64
|
+
editor.pmSchema,
|
|
65
|
+
JSON.parse(JSON.stringify(editor.prosemirrorState.doc.toJSON())),
|
|
66
|
+
fragment,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(fragment.toJSON()).toMatchInlineSnapshot(
|
|
70
|
+
// The color attributes are on the paragraph, not the blockContainer.
|
|
71
|
+
`"<blockgroup><blockcontainer id="0"><paragraph backgroundColor="red" textAlignment="right" textColor="blue">Welcome to this demo!</paragraph></blockcontainer></blockgroup>"`,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const tr = editor.prosemirrorState.tr;
|
|
75
|
+
moveColorAttributes(fragment, tr);
|
|
76
|
+
// The document will be unchanged because the color attributes are already on the paragraph.
|
|
77
|
+
expect(tr.docChanged).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("can move color attributes on older documents multiple times", async () => {
|
|
81
|
+
const doc = new Y.Doc();
|
|
82
|
+
const fragment = doc.getXmlFragment("doc");
|
|
83
|
+
const editor = BlockNoteEditor.create({
|
|
84
|
+
initialContent: [
|
|
85
|
+
{
|
|
86
|
+
type: "paragraph",
|
|
87
|
+
content: "Welcome to this demo!",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Because this was a previous schema, we are creating the YFragment manually
|
|
93
|
+
const blockGroup = new Y.XmlElement("blockGroup");
|
|
94
|
+
const el = new Y.XmlElement("blockContainer");
|
|
95
|
+
el.setAttribute("id", "0");
|
|
96
|
+
el.setAttribute("backgroundColor", "red");
|
|
97
|
+
el.setAttribute("textColor", "blue");
|
|
98
|
+
const para = new Y.XmlElement("paragraph");
|
|
99
|
+
para.setAttribute("textAlignment", "left");
|
|
100
|
+
para.insert(0, [new Y.XmlText("Welcome to this demo!")]);
|
|
101
|
+
el.insert(0, [para]);
|
|
102
|
+
blockGroup.insert(0, [el]);
|
|
103
|
+
fragment.insert(0, [blockGroup]);
|
|
104
|
+
|
|
105
|
+
// Note that the blockContainer has the color attributes, but the paragraph does not.
|
|
106
|
+
expect(fragment.toJSON()).toMatchInlineSnapshot(
|
|
107
|
+
`"<blockgroup><blockcontainer backgroundColor="red" id="0" textColor="blue"><paragraph textAlignment="left">Welcome to this demo!</paragraph></blockcontainer></blockgroup>"`,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const tr = editor.prosemirrorState.tr;
|
|
111
|
+
moveColorAttributes(fragment, tr);
|
|
112
|
+
// Note that the color attributes have been moved to the paragraph.
|
|
113
|
+
expect(JSON.stringify(tr.doc.toJSON())).toMatchInlineSnapshot(
|
|
114
|
+
`"{"type":"doc","content":[{"type":"blockGroup","content":[{"type":"blockContainer","attrs":{"id":"0"},"content":[{"type":"paragraph","attrs":{"backgroundColor":"red","textColor":"blue","textAlignment":"left"},"content":[{"type":"text","text":"Welcome to this demo!"}]}]}]}]}"`,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
el.setAttribute("backgroundColor", "green");
|
|
118
|
+
el.setAttribute("textColor", "yellow");
|
|
119
|
+
|
|
120
|
+
expect(fragment.toJSON()).toMatchInlineSnapshot(
|
|
121
|
+
`"<blockgroup><blockcontainer backgroundColor="green" id="0" textColor="yellow"><paragraph textAlignment="left">Welcome to this demo!</paragraph></blockcontainer></blockgroup>"`,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const nextTr = editor.prosemirrorState.tr;
|
|
125
|
+
moveColorAttributes(fragment, nextTr);
|
|
126
|
+
// Note that the color attributes have been moved to the paragraph.
|
|
127
|
+
expect(JSON.stringify(nextTr.doc.toJSON())).toMatchInlineSnapshot(
|
|
128
|
+
`"{"type":"doc","content":[{"type":"blockGroup","content":[{"type":"blockContainer","attrs":{"id":"0"},"content":[{"type":"paragraph","attrs":{"backgroundColor":"green","textColor":"yellow","textAlignment":"left"},"content":[{"type":"text","text":"Welcome to this demo!"}]}]}]}]}"`,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
@@ -23,14 +23,13 @@ const traverseElement = (
|
|
|
23
23
|
export const moveColorAttributes: MigrationRule = (fragment, tr) => {
|
|
24
24
|
// Stores necessary info for all `blockContainer` nodes which still have
|
|
25
25
|
// `textColor` or `backgroundColor` attributes that need to be moved.
|
|
26
|
-
const targetBlockContainers:
|
|
26
|
+
const targetBlockContainers: Map<
|
|
27
27
|
string,
|
|
28
28
|
{
|
|
29
|
-
textColor
|
|
30
|
-
backgroundColor
|
|
29
|
+
textColor: string | undefined;
|
|
30
|
+
backgroundColor: string | undefined;
|
|
31
31
|
}
|
|
32
|
-
> =
|
|
33
|
-
|
|
32
|
+
> = new Map();
|
|
34
33
|
// Finds all elements which still have `textColor` or `backgroundColor`
|
|
35
34
|
// attributes in the current Yjs fragment.
|
|
36
35
|
fragment.forEach((element) => {
|
|
@@ -40,39 +39,53 @@ export const moveColorAttributes: MigrationRule = (fragment, tr) => {
|
|
|
40
39
|
element.nodeName === "blockContainer" &&
|
|
41
40
|
element.hasAttribute("id")
|
|
42
41
|
) {
|
|
42
|
+
const textColor = element.getAttribute("textColor");
|
|
43
|
+
const backgroundColor = element.getAttribute("backgroundColor");
|
|
44
|
+
|
|
43
45
|
const colors = {
|
|
44
|
-
textColor:
|
|
45
|
-
|
|
46
|
+
textColor:
|
|
47
|
+
textColor === defaultProps.textColor.default
|
|
48
|
+
? undefined
|
|
49
|
+
: textColor,
|
|
50
|
+
backgroundColor:
|
|
51
|
+
backgroundColor === defaultProps.backgroundColor.default
|
|
52
|
+
? undefined
|
|
53
|
+
: backgroundColor,
|
|
46
54
|
};
|
|
47
55
|
|
|
48
|
-
if (colors.textColor === defaultProps.textColor.default) {
|
|
49
|
-
colors.textColor = undefined;
|
|
50
|
-
}
|
|
51
|
-
if (colors.backgroundColor === defaultProps.backgroundColor.default) {
|
|
52
|
-
colors.backgroundColor = undefined;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
56
|
if (colors.textColor || colors.backgroundColor) {
|
|
56
|
-
targetBlockContainers
|
|
57
|
+
targetBlockContainers.set(element.getAttribute("id")!, colors);
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
});
|
|
60
61
|
}
|
|
61
62
|
});
|
|
62
63
|
|
|
64
|
+
if (targetBlockContainers.size === 0) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
63
68
|
// Appends transactions to add the `textColor` and `backgroundColor`
|
|
64
69
|
// attributes found on each `blockContainer` node to move them to the child
|
|
65
70
|
// `blockContent` node.
|
|
66
71
|
tr.doc.descendants((node, pos) => {
|
|
67
72
|
if (
|
|
68
73
|
node.type.name === "blockContainer" &&
|
|
69
|
-
targetBlockContainers
|
|
74
|
+
targetBlockContainers.has(node.attrs.id)
|
|
70
75
|
) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
const el = tr.doc.nodeAt(pos + 1);
|
|
77
|
+
if (!el) {
|
|
78
|
+
throw new Error("No element found");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
tr.setNodeMarkup(pos + 1, undefined, {
|
|
82
|
+
// preserve existing attributes
|
|
83
|
+
...el.attrs,
|
|
84
|
+
// add the textColor and backgroundColor attributes
|
|
85
|
+
...targetBlockContainers.get(node.attrs.id),
|
|
86
|
+
});
|
|
76
87
|
}
|
|
77
88
|
});
|
|
89
|
+
|
|
90
|
+
return true;
|
|
78
91
|
};
|
|
@@ -12,6 +12,7 @@ import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
|
|
|
12
12
|
import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
|
|
13
13
|
import { CustomBlockNoteSchema } from "../../schema/schema.js";
|
|
14
14
|
import { UserStore } from "./userstore/UserStore.js";
|
|
15
|
+
import { getMarkRange } from "@tiptap/core";
|
|
15
16
|
|
|
16
17
|
const PLUGIN_KEY = new PluginKey(`blocknote-comments`);
|
|
17
18
|
const SET_SELECTED_THREAD_ID = "SET_SELECTED_THREAD_ID";
|
|
@@ -239,13 +240,42 @@ export class CommentsPlugin extends BlockNoteExtension {
|
|
|
239
240
|
return;
|
|
240
241
|
}
|
|
241
242
|
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
const markInSchema = view.state.schema.marks[markType];
|
|
244
|
+
const resolvedPos = view.state.doc.resolve(pos);
|
|
245
|
+
const commentMark = node.marks
|
|
246
|
+
.filter(
|
|
247
|
+
(mark) =>
|
|
248
|
+
mark.type.name === markType && mark.attrs.orphan !== true,
|
|
249
|
+
)
|
|
250
|
+
.map((mark) => {
|
|
251
|
+
// get the range of this mark within the document, to check how close it is to the click position
|
|
252
|
+
const range = getMarkRange(
|
|
253
|
+
resolvedPos,
|
|
254
|
+
markInSchema,
|
|
255
|
+
mark.attrs,
|
|
256
|
+
)!;
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
mark,
|
|
260
|
+
// calculate how far the mark is from the click position
|
|
261
|
+
distance:
|
|
262
|
+
(Math.abs(range.from - pos) + Math.abs(range.to - pos)) / 2,
|
|
263
|
+
// calculate the length of text the mark spans
|
|
264
|
+
length: range.to - range.from,
|
|
265
|
+
};
|
|
266
|
+
})
|
|
267
|
+
// This allows us to not have comments which are unreachable because they are completely overlapped by other comments (issue #2073)
|
|
268
|
+
.sort((a, b) => {
|
|
269
|
+
// Find the mark which is closest to the click position
|
|
270
|
+
if (a.distance !== b.distance) {
|
|
271
|
+
return a.distance - b.distance;
|
|
272
|
+
}
|
|
273
|
+
// Otherwise, select the mark which spans the smallest amount of text (most likely to be unreachable)
|
|
274
|
+
return a.length - b.length;
|
|
275
|
+
})[0]?.mark;
|
|
246
276
|
|
|
247
277
|
const threadId = commentMark?.attrs.threadId as string | undefined;
|
|
248
|
-
self.selectThread(threadId
|
|
278
|
+
self.selectThread(threadId);
|
|
249
279
|
},
|
|
250
280
|
},
|
|
251
281
|
}),
|
|
@@ -268,7 +298,7 @@ export class CommentsPlugin extends BlockNoteExtension {
|
|
|
268
298
|
/**
|
|
269
299
|
* Set the selected thread
|
|
270
300
|
*/
|
|
271
|
-
public selectThread(threadId: string | undefined
|
|
301
|
+
public selectThread(threadId: string | undefined) {
|
|
272
302
|
if (this.selectedThreadId === threadId) {
|
|
273
303
|
return;
|
|
274
304
|
}
|
|
@@ -280,7 +310,7 @@ export class CommentsPlugin extends BlockNoteExtension {
|
|
|
280
310
|
}),
|
|
281
311
|
);
|
|
282
312
|
|
|
283
|
-
if (threadId
|
|
313
|
+
if (threadId) {
|
|
284
314
|
const selectedThreadPosition = this.threadPositions.get(threadId);
|
|
285
315
|
|
|
286
316
|
if (!selectedThreadPosition) {
|