Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
3.8 kB · 136 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137// 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<string, any> { 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 };}