Skip to content

Commit 89669d2

Browse files
committed
chore: update excludes & mapAttributionToMark
1 parent 9acc555 commit 89669d2

4 files changed

Lines changed: 176 additions & 131 deletions

File tree

packages/core/src/extensions/tiptap-extensions/Suggestions/SuggestionMarks.ts

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { MarkSpec } from "prosemirror-model";
99
export const SuggestionAddMark = Mark.create({
1010
name: "y-attributed-insert",
1111
inclusive: false,
12-
excludes: "y-attributed-delete y-attributed-format y-attributed-insert",
12+
excludes: "",
1313
addAttributes() {
1414
return {
1515
id: { default: null, validate: "number" }, // note: validate is supported in prosemirror but not in tiptap, so this doesn't actually work (considered not critical)
@@ -61,7 +61,7 @@ export const SuggestionAddMark = Mark.create({
6161
export const SuggestionDeleteMark = Mark.create({
6262
name: "y-attributed-delete",
6363
inclusive: false,
64-
excludes: "y-attributed-delete y-attributed-format y-attributed-insert",
64+
excludes: "",
6565
addAttributes() {
6666
return {
6767
id: { default: null, validate: "number" }, // note: validate is supported in prosemirror but not in tiptap
@@ -116,16 +116,11 @@ export const SuggestionDeleteMark = Mark.create({
116116
export const SuggestionModificationMark = Mark.create({
117117
name: "y-attributed-format",
118118
inclusive: false,
119-
excludes: "y-attributed-delete y-attributed-format y-attributed-insert",
119+
excludes: "",
120120
addAttributes() {
121-
// note: validate is supported in prosemirror but not in tiptap
122121
return {
123-
id: { default: null, validate: "number" },
122+
id: { default: null, validate: "number" }, // note: validate is supported in prosemirror but not in tiptap
124123
"user-color": { default: null, validate: "string" },
125-
type: { validate: "string" },
126-
attrName: { default: null, validate: "string|null" },
127-
previousValue: { default: null },
128-
newValue: { default: null },
129124
};
130125
},
131126
extendMarkSchema(extension) {
@@ -135,28 +130,18 @@ export const SuggestionModificationMark = Mark.create({
135130
return {
136131
blocknoteIgnore: true,
137132
inclusive: false,
138-
// attrs: {
139-
// id: { validate: "number" },
140-
// type: { validate: "string" },
141-
// attrName: { default: null, validate: "string|null" },
142-
// previousValue: { default: null },
143-
// newValue: { default: null },
144-
// },
145133
toDOM(mark, inline) {
146134
return [
147135
inline ? "span" : "div",
148136
{
149137
"data-type": "modification",
150138
"data-id": String(mark.attrs["id"]),
151139
"data-user-color": String(mark.attrs["user-color"]),
152-
"data-mod-type": mark.attrs["type"] as string,
153-
"data-mod-prev-val": JSON.stringify(mark.attrs["previousValue"]),
154-
// TODO: Try to serialize marks with toJSON?
155-
"data-mod-new-val": JSON.stringify(mark.attrs["newValue"]),
156140
style:
157-
"user-color" in mark.attrs
158-
? ` --user-color: ${mark.attrs["user-color"]}`
159-
: "", // changed to "contents" to make this work for table rows
141+
(inline ? "" : "display: contents") +
142+
("user-color" in mark.attrs
143+
? `; --user-color: ${mark.attrs["user-color"]}`
144+
: ""),
160145
},
161146
0,
162147
];
@@ -170,10 +155,7 @@ export const SuggestionModificationMark = Mark.create({
170155
}
171156
return {
172157
id: parseInt(node.dataset["id"], 10),
173-
userColor: node.dataset["userColor"],
174-
type: node.dataset["modType"],
175-
previousValue: node.dataset["modPrevVal"],
176-
newValue: node.dataset["modNewVal"],
158+
"user-color": node.dataset["userColor"],
177159
};
178160
},
179161
},
@@ -185,8 +167,7 @@ export const SuggestionModificationMark = Mark.create({
185167
}
186168
return {
187169
id: parseInt(node.dataset["id"], 10),
188-
type: node.dataset["modType"],
189-
previousValue: node.dataset["modPrevVal"],
170+
"user-color": node.dataset["userColor"],
190171
};
191172
},
192173
},

packages/core/src/y/extensions/YSync.ts

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,94 @@ import {
55
} from "../../editor/BlockNoteExtension.js";
66
import { CollaborationOptions } from "./index.js";
77

8+
/**
9+
* Deterministic hash of a string to an unsigned 32-bit integer.
10+
*/
11+
const hashStr = (s: string): number => {
12+
let h = 0;
13+
for (let i = 0; i < s.length; i++) {
14+
h = ((h << 5) - h + s.charCodeAt(i)) | 0;
15+
}
16+
return h >>> 0;
17+
};
18+
19+
/**
20+
* Pick a deterministic user-color from a palette based on user ids.
21+
* Must be deterministic so the sync plugin's readback matches the mapper output.
22+
*/
23+
const userColorPalette = [
24+
"#30bced",
25+
"#6eeb83",
26+
"#ffbc42",
27+
"#ecd444",
28+
"#ee6352",
29+
"#9ac2c9",
30+
"#8acb88",
31+
"#1be7ff",
32+
];
33+
34+
const colorForUserIds = (
35+
userIds: readonly string[] | undefined | null,
36+
): string => {
37+
if (!userIds || userIds.length === 0) {
38+
return userColorPalette[0];
39+
}
40+
return userColorPalette[
41+
hashStr(String(userIds[0])) % userColorPalette.length
42+
];
43+
};
44+
45+
/**
46+
* Map a Y attribution to BlockNote's `y-attributed-*` mark attrs.
47+
*
48+
* The mapper must be deterministic in `(format, attribution)` and emit
49+
* attrs that exactly match the declared mark schema in SuggestionMarks.ts.
50+
* Any mismatch causes the sync plugin to fire phantom reconcile dispatches
51+
* in a loop. See ATTRIBUTION.md in @y/prosemirror.
52+
*
53+
* Declared attrs per mark (all three are the same shape):
54+
* - y-attributed-insert: { id, "user-color" }
55+
* - y-attributed-delete: { id, "user-color" }
56+
* - y-attributed-format: { id, "user-color" }
57+
*/
58+
const mapAttributionToMark = (
59+
format: Record<string, unknown> | null,
60+
attribution: {
61+
insert?: readonly string[];
62+
delete?: readonly string[];
63+
format?: Record<string, readonly string[]>;
64+
insertAt?: number;
65+
deleteAt?: number;
66+
formatAt?: number;
67+
},
68+
): Record<string, unknown> => {
69+
const out: Record<string, unknown> = { ...format };
70+
71+
if (attribution.insert) {
72+
out["y-attributed-insert"] = {
73+
id: attribution.insert[0] ?? null,
74+
"user-color": colorForUserIds(attribution.insert),
75+
};
76+
}
77+
78+
if (attribution.delete) {
79+
out["y-attributed-delete"] = {
80+
id: attribution.delete[0] ?? null,
81+
"user-color": colorForUserIds(attribution.delete),
82+
};
83+
}
84+
85+
if (attribution.format) {
86+
const userIds = [...new Set(Object.values(attribution.format).flat())];
87+
out["y-attributed-format"] = {
88+
id: userIds[0] ?? null,
89+
"user-color": colorForUserIds(userIds),
90+
};
91+
}
92+
93+
return out;
94+
};
95+
896
export const YSyncExtension = createExtension(
997
({
1098
options,
@@ -29,27 +117,7 @@ export const YSyncExtension = createExtension(
29117
prosemirrorPlugins: [
30118
syncPlugin({
31119
suggestionDoc: options.suggestionDoc,
32-
// // @ts-ignore types are messed up in the @y/prosemirror package right now
33-
// mapAttributionToMark(format, attribution) {
34-
// console.log("attribution", attribution);
35-
// console.log("format", format);
36-
// if (attribution.delete) {
37-
// return Object.assign({}, format, {
38-
// deletion: { id, user: attribution.delete?.[0] },
39-
// });
40-
// }
41-
// if (attribution.insert) {
42-
// return Object.assign({}, format, {
43-
// insertion: { id, user: attribution.insert?.[0] },
44-
// });
45-
// }
46-
// if (attribution.format) {
47-
// return Object.assign({}, format, {
48-
// insertion: { id, user: attribution.format?.[0] },
49-
// });
50-
// }
51-
// return format;
52-
// },
120+
mapAttributionToMark,
53121
}),
54122
],
55123
runsBefore: ["default"],

0 commit comments

Comments
 (0)