Schema & ENTER help

Hey Guys!

I’m really new to prose-mirror and still trying to get my head around some of the concepts. I’m trying to create a simple script editor for comic books, so the editor will be highly structured/opinionated. It will have a list of pages, pages have panels, panels have descriptions and speech bubbles…

My issue is when hitting enter, if the editor is currently at a page description (the starting point) it should create a nested panel description and place the cursor there, but it is then creating a second page and moving the cursor to the second page.

I may be doing some other things wrong as well here, any help or advice would be greatly appreciated!

this is everything I have at the moment.

import {EditorState} from "prosemirror-state"
import {EditorView} from "prosemirror-view"
import {Schema, DOMParser, Fragment} from "prosemirror-model"
import {keymap} from "prosemirror-keymap"

// schema
const scriptSchema = new Schema({
    nodes: {
        doc: {content: "script"},
        script: {
            content: "page+",
            toDOM(){return ["ol",{class: 'script'}, 0]},
            parseDOM: [{tag: "script"}]
        },
        page: {
            content: "pg_description panel_ls",
            parseDOM: [{tag: "page"}],
            toDOM() {return ['li', 0]}
        },
        panel_ls: {
            content: "panel*",
            toDOM() {return ['ol', {class: 'panel_ls'}, 0]}
        },
        panel: {
            content: "pl_description bubble*" ,
            parseDOM: [{tag: "panel"}],
            toDOM() { return['li', 0] }
        },
        pg_description: {
            content: "text*",
            parseDOM: [{tag: "pg_desc"}],
            toDOM() {return ['span',{class: 'desc pg_desc'}, 0]}
        },
        pl_description: {
            content: "text*",
            parseDOM: [{tag: "pl_desc"}],
            toDOM() {return ['span',{class: 'desc pl_desc'}, 0]}
        },
        bubble: {
            parseDOM: [{tag: "bubble"}],
            contents: "name speech",
            toDOM() {return ['div',{class: 'bubble'}, 0]}
        },
        name: {
            content: "text*",
            parseDOM: [{tag: "bubble_name"}],
            toDOM() {return ['div',{class: 'bubble bubble_name'}, 0]}
        },
        speech: {
            content: "text*",
            parseDOM: [{tag: "bubble_speech"}],
            toDOM() {return ['div',{class: 'bubble bubble_speech'}, 0]}
        },
        text: {}
    }
});


const NodeNames = {
    script: 'script',
    page: 'page',
    panel: 'panel',
    pg_description: 'pg_description',
    pl_description: 'pl_description',
    bubble: 'bubble',
    name: 'name',
    speech: 'speech',
    text: 'text',
    panel_ls: 'panel_ls'
}

// state map to get the next state from the current
const stateEnter = {
    [NodeNames.pg_description]: NodeNames.pl_description,
    [NodeNames.pl_description]: NodeNames.name,
    [NodeNames.name]: NodeNames.speech,
    [NodeNames.speech]: NodeNames.name
}

// key map command
function enterCommand(oState, fnDispatch) {
    console.log('Enter command');
    // return;

    // get the current node type
    const cur = oState.selection.$anchor.parent.type.name;
    console.log('CURRENT NODE : ' + cur);
    let next = stateEnter[cur];  // get the name of the node to go to


    // create the new node and insert
    // let desc = oState.doc.type.schema.nodes['description'].create();
    // let oNode = oState.doc.type.schema.nodes['panel'].createAndFill(desc);
    let oNode_ls = nodeFactory()[next](oState, cur);
    let oTransaction = oState.tr.replaceSelectionWith(oNode_ls[0]).scrollIntoView();
    // let oTransaction = oState.tr.replaceSelectionWith(oNode_ls).scrollIntoView();

    oTransaction.setMeta('nodeState', next);

    fnDispatch(oTransaction);
    // return true;
    
    function nodeFactory() {
        return {
            [NodeNames.pg_description]: function(oState) {
                // let desc = oState.doc.type.schema.nodes['description'].create();
                // let oNode = oState.doc.type.schema.nodes['panel'].createAndFill(desc);
                // return oNode
                throw new Error('Illegal state');
            },

            [NodeNames.pl_description]: function(oState) {
                const desc = oState.doc.type.schema.nodes[NodeNames.pl_description].create();
                const panel = oState.doc.type.schema.nodes[NodeNames.panel].createAndFill(desc);
                const panel_ls = oState.doc.type.schema.nodes[NodeNames.panel_ls].createAndFill(panel);
                return [desc, panel, panel_ls];
                // return [panel_ls, panel, desc];
                // return desc;
            },

            [NodeNames.name]: function(oState, cur) {
                let oNode;
                if (cur === NodeNames.pl_description) {
                    const name = oState.doc.type.schema.nodes[NodeNames.name].create();
                    oNode = oState.doc.type.schema.nodes[NodeNames.bubble].createAndFill(name);
                } else if (cur === NodeNames.speech) {
                    const name = oState.doc.type.schema.nodes[NodeNames.name].create();
                    oNode = oState.doc.type.schema.nodes[NodeNames.bubble].createAndFill(name);
                } else {
                    throw new Error('Illegal State');
                }
                // let oNode = oState.doc.type.schema.nodes['bubble'].create();
                return oNode
            },
            [NodeNames.speech]: function(oState) {
                return oState.doc.type.schema.nodes[NodeNames.speech].create();
            }
        }
    }
}

// main

window.view = new EditorView(document.querySelector("#editor"), {
  state: EditorState.create({
    doc: DOMParser.fromSchema(scriptSchema).parse(document.querySelector("#content")),
    plugins: [keymap({'Enter': enterCommand})]
  })
})

You probably need a custom command to bind to enter to create behavior like that—by default it’ll split the current node when that is possible and the node isn’t empty.

(Also, your DOM parser and serializers have some issues—they should usually round-trip, so the parse rules should match the output of the serializer, and you shouldn’t have multiple node types outputting the same DOM structure.)

Thanks for the reply. Im still a little confused, I though that was what I was doing with the lines at the bottom in the main section

plugins: [keymap({'Enter': enterCommand})]

Is there a different concept of binding commands?

The command that I think I have does the this:

// simplified from above, for the specific state

// create the new node and insert
const desc = oState.doc.type.schema.nodes[NodeNames.pl_description].create();
const panel = oState.doc.type.schema.nodes[NodeNames.panel].createAndFill(desc);
const panel_ls = oState.doc.type.schema.nodes[NodeNames.panel_ls].createAndFill(panel);

let oNode_ls = [desc, panel, panel_ls]
let oTransaction = oState.tr.replaceSelectionWith(oNode_ls[0]).scrollIntoView(); // I have also tried using 'desc' in replaceSelection

fnDispatch(oTransaction);

Thanks again for the help, I know this might be a weird specific scenario, and thanks for the advise about parsing, that will be my next task to tackle.

1 Like

Ah, right, you are binding your own command, sorry. I don’t really have time to debug it for you, though, since it’s quite a lot of code.

did you resolve the scrollIntoView issue?

The future ref, this is how I managed to solve this. Not sure it’s the best, or most elegant, but seemed to work:

    const tr = state.tr;
    const nextNode = state.schema.nodes.myNode.create();
    
    tr.replaceSelectionWith(nextNode);
    tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.$head.pos + 2)));

    dispatch(tr);