Issue with Node Type Recognition in ProseMirror for an EmbeddedEditorNodeView Using "block+" Content Model

I am experiencing an issue with a custom ProseMirror node view called EmbeddedEditorNodeView, which is intended to embed another ProseMirror editor. The node is designed to contain block-level content, hence it is configured with the "block+" content model. However, I’m encountering a specific problem where the node is incorrectly identified.

Details of the implementation and the issue are as follows:

  1. Node Specification: EmbeddedEditorNodeView is set up with a "block+" content model, intended to act as a container for an embedded ProseMirror editor.

  2. Core Issue: With the content: "block+" setting for the node, when navigating the document using the arrowHandler function, nextPos.$head.parent.type.name returns "paragraph" instead of embedded_editor_node. 20240121120741_rec_ This misidentification does not happen when the content model is set to "inline*", but my requirement is "block+". 20240121120922_rec_

  3. Attempts to Resolve: I have tried reordering node definitions, ensuring a unique and identifiable DOM structure, and refining parseDOM rules. Despite these efforts, the EmbeddedEditorNodeView is still not recognized correctly.

  4. Specific Code Implementation: Here is the specific part of the code related to the issue:

// schema{
import { schema as basicSchema } from "prosemirror-schema-basic";
import { Schema } from "prosemirror-model";
import {computePosition, flip, offset, shift,autoUpdate} from "@floating-ui/dom";

const embedded_editor_nodeSpec = {
    attrs: {
        "data-type": { default: "input" },
        "options": { default: [] },
        "data-placeholder": {}
    },
    group: "block",
    content: "block+",
    isolating:true,
    toDOM: (node) =>{
        return ["div", { class: 'embedded_editor_node',
            "data-type": node.attrs["data-type"],
            "data-placeholder": node.attrs["data-placeholder"],
            "options": JSON.stringify(node.attrs["options"]) }, 0];
    },
    parseDOM: [{
        tag: "div",  // 适用于您的 DOM 结构
        getAttrs: dom => ({
            "data-type": dom.getAttribute("data-type") || "input",
            "data-placeholder": dom.getAttribute("data-placeholder") || "",
            "options": JSON.parse(dom.getAttribute("options") || "[]")
        })
    }]
};

const embedded_editor_nodeSchema = new Schema({
    nodes: basicSchema.spec.nodes.addToStart("embedded_editor_node", embedded_editor_nodeSpec),
    marks: basicSchema.spec.marks
});

// menu{
import {insertPoint} from "prosemirror-transform"
import {MenuItem} from "prosemirror-menu"
import {buildMenuItems} from "prosemirror-example-setup"
import {Fragment} from "prosemirror-model"

let menu = buildMenuItems(embedded_editor_nodeSchema)
menu.insertMenu.content.push(new MenuItem({
    title: "Insert embedded_editor_node",
    label: "Footnote",
    select(state) {
        return insertPoint(state.doc, state.selection.from, embedded_editor_nodeSchema.nodes.embedded_editor_node) != null
    },
    run(state, dispatch) {
        let {empty, $from, $to} = state.selection, content = Fragment.empty
        if (!empty && $from.sameParent($to) && $from.parent.inlineContent)
            content = $from.parent.content.cut($from.parentOffset, $to.parentOffset)
        dispatch(state.tr.replaceSelectionWith(embedded_editor_nodeSchema.nodes.embedded_editor_node.create(null, content)))
    }
}))
// }

// nodeview_start{
import {StepMap} from "prosemirror-transform"
import {keymap} from "prosemirror-keymap"
import {undo, redo} from "prosemirror-history"

class EmbeddedEditorNodeView {
    constructor(node, view, getPos) {
        // We'll need these later
        this.node = node
        this.outerView = view
        this.getPos = getPos

        // The node's representation in the editor (empty, for now)
        this.dom = document.createElement("div")
        this.dom.classList.add("embedded_editor_node")
        this.dom.setAttribute("data-placeholder",node.attrs["data-placeholder"] || "")

        console.log("this.node",this.node)
        this.innerView = new EditorView(this.dom, {
            // You can use any node as an editor document
            state: EditorState.create({
                doc: this.node,
                plugins: [
                    ...exampleSetup({schema: embedded_editor_nodeSchema, menuContent: menu.fullMenu}),keymap({
                    "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch),
                    "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch),
                    "ArrowUp": () => this.maybeEscape("line", -1),
                    "ArrowDown":() => this.maybeEscape("line", 1),
                    "ArrowLeft": () => this.maybeEscape("char", -1),
                    "ArrowRight": () => this.maybeEscape("char", 1)
                })]
            }),
            // This is the magic part
            dispatchTransaction: this.dispatchInner.bind(this),
            handleDOMEvents: {
                mousedown: () => {
                    // Kludge to prevent issues due to the fact that the whole
                    // embedded_editor_node is node-selected (and thus DOM-selected) when
                    // the parent editor is focused.
                    if (this.outerView.hasFocus()) {
                        console.log("mousedown")
                        this.innerView.focus()
                    }
                },
                focus:(view)=> {
                }
            }
        })

        this.dataType = node.attrs["data-type"];
        this.options = node.attrs["options"];
        this.dom.setAttribute('data-type',this.dataType)
        this.dom.setAttribute('options',this.options)
    }

// nodeview_dispatchInner{
    dispatchInner(tr) {
        let {state, transactions} = this.innerView.state.applyTransaction(tr)
        this.innerView.updateState(state)

        console.log("dispatchInner",tr.getMeta("fromOutside"))
        if (!tr.getMeta("fromOutside")) {
            let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1)
            for (let i = 0; i < transactions.length; i++) {
                let steps = transactions[i].steps
                for (let j = 0; j < steps.length; j++)
                    outerTr.step(steps[j].map(offsetMap))
            }
            if (outerTr.docChanged) this.outerView.dispatch(outerTr)
        }
    }
// }
// nodeview_update{
    update(node) {
        // 更新下拉菜单的选中状态
        this.node = node;
        console.log("update",node)
        if (!node.sameMarkup(this.node)) return false
        this.node = node
        if (this.innerView) {
            let state = this.innerView.state
            let start = node.content.findDiffStart(state.doc.content)
            if (start != null) {
                let {a: endA, b: endB} = node.content.findDiffEnd(state.doc.content)
                let overlap = start - Math.min(endA, endB)
                if (overlap > 0) { endA += overlap; endB += overlap }
                this.innerView.dispatch(
                    state.tr
                        .replace(start, endB, node.slice(start, endA))
                        .setMeta("fromOutside", true))
            }
        }
        return true
    }
// }

// nodeview_setSelection{
    selectNode(){
        console.log("#selectNode")
        this.innerView.focus()
    }

    setSelection(anchor, head) {
        console.log("#setSelection",anchor,head)
        const doc = this.innerView.state.doc;
        if (anchor < 0 || anchor > doc.content.size || head < 0 || head > doc.content.size) {
            console.error('Invalid anchor or head position');
            return;
        }
        const selection = TextSelection.create(this.innerView.state.doc, anchor, head);
        this.innerView.dispatch(
            this.innerView.state.tr.setSelection(selection)
        );
        this.innerView.focus();
    }
// }

// nodeview_end{
    destroy() {
        if (this.innerView) {
            this.innerView.destroy()
            this.innerView = null
        }
    }

    stopEvent(event) {
        return this.innerView.dom.contains(event.target);
    }

    ignoreMutation() { return true }

    maybeEscape(unit, dir) {
        let { state } = this.innerView, { selection } = state;
        if (!selection.empty) return false;
        if (dir < 0 ? selection.from > 1 : selection.to < state.doc.content.size) return false;
        let targetPos = this.getPos() + (dir < 0 ? 0 : this.node.nodeSize);
        let newSelection = Selection.near(this.outerView.state.doc.resolve(targetPos), dir);
        this.outerView.dispatch(this.outerView.state.tr.setSelection(newSelection).scrollIntoView());
        this.outerView.focus();
        return true;
    }

}

import {EditorState,Selection,TextSelection} from "prosemirror-state"
import {DOMParser} from "prosemirror-model"
import {EditorView} from "prosemirror-view"
import {exampleSetup} from "prosemirror-example-setup"


function arrowHandler(dir) {
    return (state, dispatch, view) => {
        if (state.selection.empty && view.endOfTextblock(dir)) {
            let side = dir == "left" || dir == "up" ? -1 : 1
            let $head = state.selection.$head
            console.log("$head",$head)
            console.log("$head.after()",$head.after())
            console.log("$head.before()",$head.before())
            let nextPos = Selection.near(
                state.doc.resolve(side > 0 ? $head.after() : $head.before()), side)
            console.log("#nextPos.$head.parent.type.name:",nextPos.$head.parent.type.name)
            if (nextPos.$head && nextPos.$head.parent.type.name == "embedded_editor_node") {
                dispatch(state.tr.setSelection(nextPos))
                return true
            }
        }
        return false
    }
}

const arrowHandlers = keymap({
    ArrowLeft: arrowHandler("left"),
    ArrowRight: arrowHandler("right"),
    ArrowUp: arrowHandler("up"),
    ArrowDown: arrowHandler("down")
});

window.view = new EditorView(document.querySelector("#editor"), {
    state: EditorState.create({
        doc: DOMParser.fromSchema(embedded_editor_nodeSchema).parse(document.querySelector("#content")),
        plugins: exampleSetup({schema: embedded_editor_nodeSchema, menuContent: menu.fullMenu}).concat(arrowHandlers)
    }),
    nodeViews: {
        embedded_editor_node(node, view, getPos) {
            return new EmbeddedEditorNodeView(node, view, getPos);
        }
    }
})

<!DOCTYPE html>
<html>
<head>
  <title>ProseMirror Editor</title>
  <meta charset="UTF-8">

  <script src="dist/prosemirror-bundle.min.js" defer></script>
  <link rel="stylesheet" href="dist/editor.css">
</head>
<style>
  .embedded_editor_node {
    border: 1px solid gray;
    border-radius: 5px;
    padding: 0px 10px;
    margin: 0 5px;
    display: inline-block;
  }
</style>
<body>
<div id="editor"></div>

<div id="content" style="display: none">
  xxx
  <div class="embedded_editor_node">zzzz</div>
  yyyy
</div>
</body>
</html>

I suspect this problem might be related to how ProseMirror parses and interprets the nested editor structure within EmbeddedEditorNodeView. However, I am uncertain about the exact cause or the solution.

I am providing the code for a more detailed reference. Could anyone offer insights or suggestions on how to correctly implement this nested node structure, ensuring that an EmbeddedEditorNodeView with a "block+" content model is accurately recognized and not confused with a paragraph?

Any advice or guidance would be greatly appreciated!