Getting the to/from positions for a given mark

Hello, How would one go about getting the absolute position of a mark when the mouse is not focused or selected on that tag? Is this possible with Prosemirror?

For more context as to the specific use case of this… I have a custom schema that tacks on a guid as well as some other attributes to the mark. These are then rendered in a component that is separate from the editor. From this rendered component, I would like to be able to delete/update the marks and have that be reflected in the schema presented by the editor, but I would need to know their absolute positions in the editor so I know which parts of the schema need to be updated. I would hope that one of the ways listed below would be a valid way to go about getting the absolute position of a given mark, but I’m not sure if this is something Prosemirror is capable of.

Way 1:

 const mark = document.getElementById(this.guid);
// get the to and from positions off of the constant mark.

Way 2: Within the schema, storing the absolute position on a data attribute. (I could save this whenever a mark is added since I am toggling marks on selected ranges, but that wouldn’t account for copying/pasting that mark around the document or editing text around the mark that would ultimately change it’s absolute position)

get schema(): MarkSpec {
    return {
      attrs: {
        attributes: {}
      },
      toDOM: (node: any) => [
        'mark',
        {
          'data-value': JSON.parse(node.attrs.attributes).value,
          id: JSON.parse(node.attrs.attributes).guid,
          from: // Somehow get the from selection,
          to: // Somehow get the to selection
        }
      ],
      parseDOM: [
        {
         ...
         ...
         ...
        }
      ]
    };
  }

The absolute position from and to are document dependent, and I would caution against saving those in the schema as done in way-2. This adds unnecessary complexity and will be much hard to maintain when the positions become stale.

I am not sure what the most pragmatic approach will be, but here is my attempt:

// uid is a string which uniquely identifies the mark you are looking for
export function findMarkByUid(editorState, from, to, uid) {
  let found;
  editorState.doc.nodesBetween(from, to, (node) => {
   if (found) return false; 
   found = node.marks.find(mark => mark.attrs['data-uid'] === uid)
  });

  return found
}

// somewhere in your mark schema
toDOM: () => ['em', {'data-uid': generateUID()}, 0],

If you want the document wide positions, you can do something like:

function findMarkPosition(mark, doc, from, to) {
  let markPos = { start: -1, end: -1 };
  doc.nodesBetween(from, to, (node, pos) => {
    // stop recursing if result is found
    if (markPos.start > -1) {
      return false;
    }
    if (markPos.start === -1 && mark.isInSet(node.marks)) {
      // expect to see something like `italic('my text')`
      console.log(node.toString())
      markPos = {
        start: pos,
        end: pos + Math.max(node.textContent.length, 1),
      };
    }
  });

  return markPos;
}
2 Likes

Hello, have you ever come up with the solution. I have a very fancy scenario. To oversimplify: I want to delete a mark on click. And just to add I neither not have a selection in the editor not the cursor I used markView to add the ‘click’ event on the mark element

this.view = new EditorView(document.querySelector('#editor'), {
      state,
      markViews: {
        addedContent: (mark, dom, inline) => {
          return new MarkPopupViewBuilder(mark, editor, inline);
        }
      }
    });

and then I thought I can use Mark instance and be sure it will be equal to exact mark in the document.

export class MarkPopupViewBuilder implements ReturnType<MarkViewConstructor> {
  public dom: HTMLElement;
  public mark: Mark;
  public view: EditorView;
  public inline: boolean;

  constructor(mark: Mark, view: EditorView, inline: boolean) {
    this.mark = mark;
    this.view = view;
    this.inline = inline;

    if (mark.type.spec.toDOM) {
      const { dom } = DOMSerializer.renderSpec(document, mark.type.spec.toDOM(mark, inline), null);

      this.dom = dom as HTMLElement;
    }

    this.dom.addEventListener('click', () => {
      const { state, dispatch } = this.view;
      
      const tr = state.tr;
    
      state.doc.nodesBetween(0, state.doc.content.size, (node, pos) => {
        if (mark.isInSet(node.marks)) { //Look this line <-------------------------------------------
          tr.removeMark(pos, pos + node.nodeSize, mark.type);
        }
      });
      dispatch(tr);
    } );
  }
}

But apparently it’s not, if mark doesn’t have any special attributes it will match all such marks in the document.

I am looking for a way how can I get the position of this mark in the text and toggle/remove it being sure no other locations in the document will be affected. So far only the solution with custom id attribute I have but it begins messy when we deal with ctrl-c+v

1 Like