Updating mark attributes

I’m making a storytelling application that uses ProseMirror with a custom mark. This mark has one attribute, targetCharacters, which contains a list of characters the marked text refers to. So simply toggling it on and off doesn’t really make much sense: sometimes you want to add or remove a character from the list, ie. update the value of the targetCharacters attribute.

I’m not sure what the most sensible way to do this is. My first attempt was to remove the mark, then re-add it if there is at least one character left in the list (I have a special UI to choose characters from a list). But the problem is, there is no tr.modifyMark (only tr.addMark and tr.removeMark) and I cannot call toggleMark twice in the callback in openPrompt (I get an Applying a mismatched transaction)…

Any ideas or pointers?

Yes, you’ll have to remove and re-add the mark to change it. You’ll probably want to avoid toggleMark and write your own command for this, on top of addMark and removeMark.

1 Like

Excellent, thank you so much! I got confused and thought there could only be one step in the dispatch call (now, if I understand it correctly, I can create steps and call dispatch on the last; at least that seems to work).

Without this reply I would have tried a completely different, wrong approach and wasted lots of time. Thanks again!

I made a plugin for updating image node’s alt attr:

image

The form handler to ProseMirror Transaction looks like:

    const anchor = state.selection.$anchor;
    if (state.selection instanceof NodeSelection && state.selection.node.type.name === "image") {
      const node = state.selection.node;
      editableAttrs = { alt: node.attrs.alt };
      editableAttrsHandler = (e) => {
        e.preventDefault();
        const formData = new FormData(e.target);
        editorView.dispatch(
          state.tr.setNodeMarkup(anchor.pos, undefined, { ...node.attrs, alt: formData.get("alt") })
        );
      };
    }

I’m want to do something similar for links: a tooltip for editing the href attr:

image

Any hints about how to do this in 2021? Edit: I got the following code working. Hints still welcome… it feels like a bit much to change a link.

    const anchor = state.selection.$anchor;
    let showLinkMenu = false;
    const linkMarkInfo = markPosition(state, anchor.pos, state.schema.marks.link);
    if (linkMarkInfo) {
      showLinkMenu = true;
      const { from, to, mark } = linkMarkInfo;
      editableAttrs = { href: mark.attrs.href };
      editableAttrsHandler = (e) => {
        e.preventDefault();
        const formData = new FormData(e.target);
        const transaction = state.tr;
        // Can't just edit a mark attr, we have to remove then add
        transaction.removeMark(from, to, mark);
        const href = formData.get("href");
        // Empty string can just remove the mark though
        if (typeof href === "string" && href.trim() !== "") {
          mark.attrs = { ...mark.attrs, href };
          transaction.addMark(from, to, mark);
        }
        editorView.dispatch(transaction);
      };
    }

// Via: https://discuss.prosemirror.net/t/expanding-the-selection-to-the-active-mark/478/9
function markPosition(state/**: EditorState*/, pos/**: number*/, markType/**: MarkType*/) {
  const $pos = state.doc.resolve(pos);

  const { parent, parentOffset } = $pos;
  const start = parent.childAfter(parentOffset);
  if (!start.node) return;

  const mark = start.node.marks.find((mark) => mark.type === markType);
  if (!mark) return;

  let startIndex = $pos.index();
  let from = $pos.start() + start.offset;
  let endIndex = startIndex + 1;
  let to = from + start.node.nodeSize;
  while (startIndex > 0 && mark.isInSet(parent.child(startIndex - 1).marks)) {
    startIndex -= 1;
    from -= parent.child(startIndex).nodeSize;
  }
  while (endIndex < parent.childCount && mark.isInSet(parent.child(endIndex).marks)) {
    to += parent.child(endIndex).nodeSize;
    endIndex += 1;
  }
  return { from, to, mark };
}