Phantom Space Behavior at Paragraph Boundaries When Applying Marks

I’m encountering an issue in ProseMirror where marks (like font, bold, etc.) create “phantom space” at the start and end of paragraph nodes. Specifically, when I apply marks to text, and the cursor is at the beginning or end of a paragraph, ProseMirror behaves as if there’s an invisible space at the boundary, even though no such space exists in the HTML or document structure.

Steps to Reproduce:

  1. Apply marks to text (like fontFamily, fontSize, bold, textColor).
  2. Place the cursor at the start or end of a paragraph.
  3. Press the right arrow at the end of the paragraph or the left arrow at the start of the paragraph.
  4. The cursor behaves as though there’s already a space at the boundary (phantom space), even though it’s not visible in the DOM.
  5. If you try to delete or backspace around the boundaries, the space comes back.

Expected Behavior:

  • Marks should only be applied to actual content, and no phantom space should appear when the cursor is at the start or end of a paragraph.
  • When using setStoredMarks or addMark, the behavior should not affect the cursor movement at the paragraph boundary.

Question:

  • Is there a recommended way to handle mark application at paragraph boundaries without causing this “phantom space” behavior?
  • Could this be an issue with how ProseMirror is applying marks across block boundaries?

Any guidance or suggestions would be greatly appreciated!

There’s no such phantom space in this system, and I’m having a hard time understanding what the issue is. Are you talking about the fact that it takes an additional press of shift-left/shift-right to move the selection head past a paragraph boundary?

Yes, exactly. If the caret is at the start of the paragraph, it takes an additional press of shift-left to move the selection like you mentioned. When I press once, the applied marks are gone. When I press again, it moves out of the paragraph. It’s as if the paragraph has a surrounding space which doesn’t have the marks I applied on the paragraph using addMark() or setStoredMarks().

Here is the function responsible for handling the marks. May be it will help to resolve the issue. I use setStoredMarks() and addMark() at the end of the function that are causing this problem.

export const applyOutputstyle = (view, tr, from, to, outputstylesCSS, outputstyle, useStoredMarks = false) => {
  let trNew = tr ? tr : view.state.tr;
  if (!view.state.doc.nodeAt(from)) // For new nodes, -1 works
    from = from - 1
  const node = view.state.doc.nodeAt(from);

  if (!node) return;

  // Collect updated node attributes
  let updatedAttrs = { ...node.attrs, outputstyle: outputstyle };

  if (outputstylesCSS["textAlign"]) {
    updatedAttrs.nodeTextAlignment = outputstylesCSS["textAlign"];
  }
  if (outputstylesCSS["lineHeight"]) {
    updatedAttrs.lineHeight = outputstylesCSS["lineHeight"];
  }

  let styleString = extractSpecifiedStyles(outputstylesCSS, null, true);

  updatedAttrs.style = styleString;

  // Apply node attribute updates in a single call
  trNew = trNew.setNodeMarkup(from, null, updatedAttrs);

  // Prepare marks
  const marks = [];
  if (outputstylesCSS["fontFamily"]) {
    marks.push(view.state.schema.marks.fontFamily.create({ fontFamily: outputstylesCSS["fontFamily"] }));
  }
  if (outputstylesCSS["fontSize"]) {
    marks.push(view.state.schema.marks.fontSize.create({ size: outputstylesCSS["fontSize"] }));
  }
  if (outputstylesCSS["fontWeight"] === "bold") {
    marks.push(view.state.schema.marks.bold.create());
  }
  if (outputstylesCSS["color"]) {
    marks.push(view.state.schema.marks.textColor.create({ color: outputstylesCSS["color"] }));
  }

  // Apply marks using addMark or setStoredMarks
  if (useStoredMarks) {
    let currentMarks = view.state.storedMarks || [];
    let updatedMarks = [
      ...currentMarks.filter(mark => !marks.some(newMark => newMark.type === mark.type)),
      ...marks
    ];
    trNew = trNew.setStoredMarks(updatedMarks);
  } else {
    marks.forEach(mark => {
      trNew = trNew.addMark(from, to, mark);
    });
  }

  return trNew;
};

Which browser is this happening on? And can you reproduce it in the demo editor on prosemirror.net (which has buttons to add marks)? If so, please provide the exact steps.