How to avoid copying attributes to new paragraph

I’ve set up the schema and a plugin to assign a unique ID to each paragraph. The schema includes this attribute blockId:

  paragraph: {
    attrs: { blockId: { default: null } },
    content: 'inline*',
    group: 'block',
    parseDOM: [
      {
        tag: 'p',
        getAttrs(dom) {
          const p = dom as HTMLParagraphElement;
          return {
            blockId: p.getAttribute('data-block-id'),
          };
        },
      },
    ],
    toDOM(node: ProsemirrorNode) {
      const domAttrs = {
        'data-block-id': node.attrs.blockId,
      };
      return ['p', domAttrs, 0];
    },
  },

and it gets assigned by this plugin:

export const blockIdPlugin = new Plugin({
  appendTransaction: (transactions, _prevState, nextState) => {
    const tr = nextState.tr;
    let modified = false;
    if (transactions.some((transaction) => transaction.docChanged)) {
      nextState.doc.descendants((node, pos) => {
        if ('blockId' in node.attrs && node.attrs.blockId == null) {
          tr.setNodeMarkup(pos, undefined, { ...node.attrs, blockId: nanoid() });
          modified = true;
        }
      });
    }

    return modified ? tr : null;
  },
});

However, when I create new empty paragraphs in the editor (press Enter a few times), only the first paragraph gets assigned a unique ID and the subsequent paragraphs reuse the same attribute values. Does ProseMirror automatically copy attribute values to new nodes? Is there a way to control this behaviour?

1 Like

Yes, splitBlock (which is bound to enter by default) will split the current block at the cursor position. There is no way to guarantee unique IDs on the level of nodes and attributes (these will also be duplicated when you copy-paste, drag, etc), but you can create an appendTransaction hook that will check and fix up id uniqueness if you need this.

2 Likes

I was able to make them unique by following your suggestion – modified the plugin to ensure IDs are unique:

export const blockIdPlugin = new Plugin({
  appendTransaction: (transactions, prevState, nextState) => {
    const blockIds = new Set<string>();
    const tr = nextState.tr;
    let modified = false;

    if (transactions.some((transaction) => transaction.docChanged)) {
      nextState.doc.descendants((node, pos) => {
        if ('blockId' in node.attrs) {
          let blockId = node.attrs.blockId;
          if (blockId == null || blockIds.has(blockId)) {
            blockId = nanoid();
            tr.setNodeMarkup(pos, undefined, { ...node.attrs, blockId });
            modified = true;
          }
          blockIds.add(blockId);
        }
      });
    }

    return modified ? tr : null;
  },
});

Thanks for the quick reply!

1 Like