Duplicate node after pasting

I am trying to implement spoiler and call it via inputRule

The basic diagram looks like this

const nodes: {
  doc: {
    content: 'block+'
  } as NodeSpec,
  
  paragraph: {
    attrs: { style: { default: '' } },
    content: 'inline*',
    group: 'block',
    parseDOM: [{ tag: 'p' }],
    toDOM(node) {
      return ['p', 0];
    }
  } as NodeSpec,
  
  spoiler: {
    attrs: {
      label: {
        default: ''
      }
    },
    content: 'block+',
    group: 'block',
    parseDOM: [
      { tag: 'div' },
      { class: 'spoiler' },
      ['div', { class: 'spoiler__head', getAttrs: (dom) => getTitleAttrs(dom) }, 0],
      ['div', { class: 'spoiler__content' }, 0]
    ],
    toDOM(node) {
      return [
        'div',
        { class: 'spoiler', ...getTitleAttrs(node) },
        ['div', { class: 'spoiler__head' }],
        ['div', { class: 'spoiler__content' }, 0]
      ];
    }
  } as NodeSpec
}

function getTitleAttrs(node) {
  return {
    'data-title': node.attrs.label
  };
}

Editor initialization


let state = EditorState.create({
  schema: main_schema,
  plugins: [
    inputRules({
      rules: [wrappingInputRule(/^\s*=\s$/, main_schema.nodes.spoiler)]
    }),
    ...exampleSetup({ schema: main_schema })
  ]
});

const editor_view = new EditorView(this.el, {
  state: state,
  dispatchTransaction: (transaction) => {
    editor_view.updateState(editor_view.state.apply(transaction));
    ...
  }
});

Spoiler View

function generate_spoiler(context, node, view, getPos) {
  // Main spoiler node
  let node_wrap = (context.dom = document.createElement('div'));
  node_wrap.addClass('spoiler');

  // Header wrapper
  let title_wrap = document.createElement('div');
  title_wrap.addClass('spoiler__head');

  // Content container
  let content_wrap = (context.contentDOM = document.createElement('div'));
  content_wrap.addClass('spoiler__content');

  // Input field
  let title_input = (context.input_node = document.createElement('input'));
  title_input.addClass('spoiler__input');

  // Delete button
  let delete_button = document.createElement('button');
  delete_button.innerText = 'Удалить';

  // toggle button
  let toggle_button = document.createElement('button');
  content_wrap.addClass('spoiler__toggle');
  toggle_button.innerText = '>';

  node_wrap.appendChild(title_wrap);
  node_wrap.appendChild(content_wrap);
  node_wrap.appendChild(delete_button);
  title_wrap.appendChild(toggle_button);
  title_wrap.appendChild(title_input);

  delete_button.addEventListener('click', (e) => {
    view.dispatch(view.state.tr.delete(getPos(), getPos() + context.nodeSize));
    e.preventDefault();
  });

  title_input.addEventListener('input', ({ target }) => {
    let tra = view.state.tr.setNodeMarkup(getPos(), null, {
      label: (<HTMLInputElement>target)!.value
    });
    view.dispatch(tra);
  });
}

export class SpoilerView {
  declare dom: HTMLElement;
  declare contentDOM: HTMLElement;
  nodeSize: number;
  input_node!: HTMLInputElement;

  constructor(node, view: EditorView, getPos) {
    this.nodeSize = node.nodeSize;
    generate_spoiler(this, node, view, getPos);
    this.input_node.value = node.attrs.label;
  }

  update(node) {
    if (node.type.name !== 'spoiler') return false;
    this.nodeSize = node.nodeSize;
    this.input_node.value = node.attrs.label;
    return true;
  }
}

This design generally works well, include undo and redo. Exactly before trying to copy and paste back.

Initial construction and expected construction after insertion:

<spoiler>
  <spoiler_head>
    <input>
  </spoiler_head>
  <spoiler_content>
   <p>Entered content</p>
 </spoiler_content>
</spoiler>

Real design after pasting

<spoiler>
  <spoiler_head>
    <input>
  </spoiler_head>
  <spoiler_content>

    <spoiler>
      <spoiler_head>
        <input>
      </spoiler_head>
      <spoiler_content></spoiler_content>
    </spoiler>
    
    <spoiler>
      <spoiler_head>
        <input>
      </spoiler_head>
      <spoiler_content>
       <p>Entered content</p>
     </spoiler_content>
    </spoiler>

 </spoiler_content>
</spoiler>

This also happens if you disable nodeViews

In addition, when copying, attributes are not preserved.

What could be the reason for this behavior?

It looks like your parseDOM rules are matching multiple nested elements created by your toDOM function for a single spoiler, leading to the creation of multiple spoiler nodes.