// Taken from https://github.com/ueberdosis/tiptap/blob/dfacb3b987b57b3ab518bae87bc3d263ebfb60d0/packages/core/src/commands/setMark.ts#L66 import { EditorState, TextSelection, Transaction } from "prosemirror-state"; import { Mark, MarkType, ResolvedPos, Schema } from "prosemirror-model"; function canSetMark( state: EditorState, tr: Transaction, newMarkType: MarkType, ) { const { selection } = tr; let cursor: ResolvedPos | null = null; if (selection instanceof TextSelection) { cursor = selection.$cursor; } if (cursor) { const currentMarks = state.storedMarks ?? cursor.marks(); // There can be no current marks that exclude the new mark return ( !!newMarkType.isInSet(currentMarks) || !currentMarks.some((mark) => mark.type.excludes(newMarkType)) ); } const { ranges } = selection; return ranges.some(({ $from, $to }) => { let someNodeSupportsMark = $from.depth === 0 ? state.doc.inlineContent && state.doc.type.allowsMarkType(newMarkType) : false; state.doc.nodesBetween($from.pos, $to.pos, (node, _pos, parent) => { // If we already found a mark that we can enable, return false to bypass the remaining search if (someNodeSupportsMark) { return false; } if (node.isInline) { const parentAllowsMarkType = !parent || parent.type.allowsMarkType(newMarkType); const currentMarksAllowMarkType = !!newMarkType.isInSet(node.marks) || !node.marks.some((otherMark) => otherMark.type.excludes(newMarkType)); someNodeSupportsMark = parentAllowsMarkType && currentMarksAllowMarkType; } return !someNodeSupportsMark; }); return someNodeSupportsMark; }); } export const setMark = (type: MarkType, attributes = {}) => (state: EditorState, dispatch: (tr: Transaction) => void) => { const { selection } = state; const { empty, ranges } = selection; let tr = state.tr; if (empty) { const oldAttributes = getMarkAttributes(state, type); tr.addStoredMark( type.create({ ...oldAttributes, ...attributes, }), ); } else { ranges.forEach((range) => { const from = range.$from.pos; const to = range.$to.pos; state.doc.nodesBetween(from, to, (node, pos) => { const trimmedFrom = Math.max(pos, from); const trimmedTo = Math.min(pos + node.nodeSize, to); const someHasMark = node.marks.find((mark) => mark.type === type); // if there is already a mark of this type // we know that we have to merge its attributes // otherwise we add a fresh new mark if (someHasMark) { node.marks.forEach((mark) => { if (type === mark.type) { tr.addMark( trimmedFrom, trimmedTo, type.create({ ...mark.attrs, ...attributes, }), ); } }); } else { tr.addMark(trimmedFrom, trimmedTo, type.create(attributes)); } }); }); } dispatch(tr); }; function getMarkAttributes( state: EditorState, type: MarkType, ): Record { const { from, to, empty } = state.selection; const marks: Mark[] = []; if (empty) { if (state.storedMarks) { marks.push(...state.storedMarks); } marks.push(...state.selection.$head.marks()); } else { state.doc.nodesBetween(from, to, (node) => { marks.push(...node.marks); }); } const mark = marks.find((markItem) => markItem.type.name === type.name); if (!mark) { return {}; } return { ...mark.attrs }; }