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}