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 };
}
1 Like

Hi - I’m relatively new to ProseMirror so please excuse my newbie level of understanding. I am trying to build a custom mark. I have the same basic problem that is discussed in this thread – I need to update the attributes for a mark. My custom mark has a single attribute named ‘reference’ that I want to validate and possibly update when the content contained by the mark is updated. To modify the mark attributes, I think I need to call removeMark and addMark (as discussed in the thread). My attempt is below, but it doesn’t seem to update the view / state. Am I doing something obviously wrong? Here is my onUpdate function:

    onUpdate() {
        if (this.options.updatingView) return;
        const state = this.editor.view.state;
        const selection = state.selection;
        const marks = state.doc.resolve(selection.to).marks();
        if (this.type.isInSet(marks)) {
            this.options.updatingView = true;
            const tr = state.tr;
            tr.removeMark(selection.from, selection.to, this.type);
            const newMark = this.type.create({ reference: 'boo-hoo' });
            tr.addMark(selection.from, selection.to, newMark);
            this.editor.view.dispatch(tr);
            this.options.updatingView = false;
            console.log(JSON.stringify(this.editor.view.state.toJSON()));
        }
    },

Figured this out. My newMark didn’t have the right to/from extents. Was able to get the right extents to add the new mark with the resolve method:

		const state = this.editor.view.state;
		const selection = state.selection;
		const pos = state.doc.resolve(selection.to);
		const start = pos.start();
		const end = pos.end();

TypeScript was screaming at me for mutating a ReadOnly property of a Mark like this:

if (typeof href === "string" && href.trim() !== "") {
  mark.attrs = { ...mark.attrs, href };
  transaction.addMark(from, to, mark);
}

I think the recommended way is to create a new mark using the schema

const newMark = state.schema.marks.link.create({ ...mark.attrs, href });
transaction.addMark(from, to, newMark);