I’m trying to highlight a mark with an inline decoration whenever the selection is completely inside of said mark and am having partial success.
If the $anchor
and $head
are both contained within the same mark instance, selection.empty
or not, then things work as expected. But as soon as the selection is not empty and the $head
leaves either side of the mark, then the selection jumps to that side of the mark or just goes haywire with the decorations. The really confusing thing is that this behavior only happens when selecting with a mouse. If I use the keyboard to change the selection, then everything works as expected. I’ve added an example of each situation below. I’d appreciate any ideas that anyone might have about what I’m doing wrong.
My plugin looks like this:
import {Plugin} from 'prosemirror-state';
import {Decoration, DecorationSet} from 'prosemirror-view';
import {getMarkRange} from '@tiptap/core';
import {CommentsPluginKey} from './keys';
return new Plugin({
key: CommentsPluginKey,
state: {
init() {
return {activeCommentId: null};
},
apply(tr, pluginState, oldState, newState) {
const {doc, selection} = tr;
const {$from, to, empty} = selection;
const $to = doc.resolve(empty ? to + 1 : to);
const {name: markTypeName} = newState.schema.marks.comments;
const fromMark = $from.marks().find((mark) => mark.type.name === markTypeName);
const toMark = $to.marks().find((mark) => mark.type.name === markTypeName);
let activeCommentId = null;
if ((fromMark && toMark) && (fromMark === toMark)) {
// If both ends are on a comment, check if it's the same instance of a mark,
// not a mark that has been split or duplicated, e.g. with copy and paste
activeCommentId = toMark.attrs.commentId;
} else if (selection.empty && toMark) {
// Otherwise, if the selection is empty and there's a mark at the next position, then select that one
activeCommentId = toMark.attrs.commentId;
}
return {activeCommentId};
},
},
props: {
decorations(state) {
const {activeCommentId} = this.getState(state);
const {doc, schema} = state;
const {name: markTypeName} = schema.marks.comments;
if (activeCommentId === null) {
return DecorationSet.empty;
}
// If there is a comment, then find all its matches across the doc
const activeMarks = [];
doc.descendants((node, pos) => {
node.marks.forEach((mark) => {
if (mark.type.name === markTypeName
&& mark.attrs.commentId === activeCommentId) {
activeMarks.push({
mark,
$pos: doc.resolve(pos),
});
}
});
});
const decorations = [];
activeMarks.forEach(({mark, $pos}) => {
const markRange = getMarkRange($pos, mark.type, mark.attrs);
if (markRange) {
decorations.push(Decoration.inline(markRange.from, markRange.to, {class: 'comment-highlight'}, {...mark.attrs}));
}
});
return DecorationSet.create(doc, decorations);
},
},
});