a tool for shared writing and social publishing
1// Taken from https://github.com/ueberdosis/tiptap/blob/dfacb3b987b57b3ab518bae87bc3d263ebfb60d0/packages/core/src/commands/setMark.ts#L66
2
3import { EditorState, TextSelection, Transaction } from "prosemirror-state";
4import { Mark, MarkType, ResolvedPos, Schema } from "prosemirror-model";
5
6function canSetMark(
7 state: EditorState,
8 tr: Transaction,
9 newMarkType: MarkType,
10) {
11 const { selection } = tr;
12 let cursor: ResolvedPos | null = null;
13
14 if (selection instanceof TextSelection) {
15 cursor = selection.$cursor;
16 }
17
18 if (cursor) {
19 const currentMarks = state.storedMarks ?? cursor.marks();
20
21 // There can be no current marks that exclude the new mark
22 return (
23 !!newMarkType.isInSet(currentMarks) ||
24 !currentMarks.some((mark) => mark.type.excludes(newMarkType))
25 );
26 }
27
28 const { ranges } = selection;
29
30 return ranges.some(({ $from, $to }) => {
31 let someNodeSupportsMark =
32 $from.depth === 0
33 ? state.doc.inlineContent && state.doc.type.allowsMarkType(newMarkType)
34 : false;
35
36 state.doc.nodesBetween($from.pos, $to.pos, (node, _pos, parent) => {
37 // If we already found a mark that we can enable, return false to bypass the remaining search
38 if (someNodeSupportsMark) {
39 return false;
40 }
41
42 if (node.isInline) {
43 const parentAllowsMarkType =
44 !parent || parent.type.allowsMarkType(newMarkType);
45 const currentMarksAllowMarkType =
46 !!newMarkType.isInSet(node.marks) ||
47 !node.marks.some((otherMark) => otherMark.type.excludes(newMarkType));
48
49 someNodeSupportsMark =
50 parentAllowsMarkType && currentMarksAllowMarkType;
51 }
52 return !someNodeSupportsMark;
53 });
54
55 return someNodeSupportsMark;
56 });
57}
58export const setMark =
59 (type: MarkType, attributes = {}) =>
60 (state: EditorState, dispatch: (tr: Transaction) => void) => {
61 const { selection } = state;
62 const { empty, ranges } = selection;
63
64 let tr = state.tr;
65 if (empty) {
66 const oldAttributes = getMarkAttributes(state, type);
67
68 tr.addStoredMark(
69 type.create({
70 ...oldAttributes,
71 ...attributes,
72 }),
73 );
74 } else {
75 ranges.forEach((range) => {
76 const from = range.$from.pos;
77 const to = range.$to.pos;
78
79 state.doc.nodesBetween(from, to, (node, pos) => {
80 const trimmedFrom = Math.max(pos, from);
81 const trimmedTo = Math.min(pos + node.nodeSize, to);
82 const someHasMark = node.marks.find((mark) => mark.type === type);
83
84 // if there is already a mark of this type
85 // we know that we have to merge its attributes
86 // otherwise we add a fresh new mark
87 if (someHasMark) {
88 node.marks.forEach((mark) => {
89 if (type === mark.type) {
90 tr.addMark(
91 trimmedFrom,
92 trimmedTo,
93 type.create({
94 ...mark.attrs,
95 ...attributes,
96 }),
97 );
98 }
99 });
100 } else {
101 tr.addMark(trimmedFrom, trimmedTo, type.create(attributes));
102 }
103 });
104 });
105 }
106
107 dispatch(tr);
108 };
109
110function getMarkAttributes(
111 state: EditorState,
112 type: MarkType,
113): Record<string, any> {
114 const { from, to, empty } = state.selection;
115 const marks: Mark[] = [];
116
117 if (empty) {
118 if (state.storedMarks) {
119 marks.push(...state.storedMarks);
120 }
121
122 marks.push(...state.selection.$head.marks());
123 } else {
124 state.doc.nodesBetween(from, to, (node) => {
125 marks.push(...node.marks);
126 });
127 }
128
129 const mark = marks.find((markItem) => markItem.type.name === type.name);
130
131 if (!mark) {
132 return {};
133 }
134
135 return { ...mark.attrs };
136}