Replace initial/default document paragraph with something else

Hi,

With the latest code (pull from github, which I assume is about the same as 0.6.0 release), I created an editor with an empty content initially. So I get a doc with an empty paragraph created:

JSON.stringify(pm.doc.toJSON()) -->

“{“type”:“doc”,“content”:[{“type”:“paragraph”}]}”

If I try to replace the automatically created paragraph with my own:

pm.tr.replaceSelection(pm.schema.node(‘paragraph’)).apply()

it actually appends it:

JSON.stringify(pm.doc.toJSON()) -->

“{“type”:“doc”,“content”:[{“type”:“paragraph”},{“type”:“paragraph”}]}”

The real story is that I’m replacing the initial paragraph with my own custom node, but the problem is reproduced with this much simpler example.

If this normal behavior or is it a bug?

Thanks! Boris

If the selection at the point is still at the default initial selection, you’re not actually replacing the old paragraph, but rather trying to insert a paragraph into the existing paragraph (which will lead to the two paragraphs you got). Something like this can change the type of the initial empty paragraph:

pm.tr.setNodeType(0, pm.schema.nodes.paragraph).apply()

Or just pass a doc option with the start document that you want.

Is there any way one can initiate with an entirely empty doc? Meaning one that doesn’t even have a single paragraph in it, so it won’t allow editing until at least one more is added?

Setting the doc/docFormat doesn’t seem to help.

No, that’s not possible. There’s an invariant that only documents that contain at least one valid selection position are allowed.

Hi,

Actually my problem is not so much with the initial paragraph. I just tried to simplify the use case as much as possible before posting my question. But in general, I have a situation where I’m at a newly started paragraph and I want to implement a command that replaces it with something else. So I have an empty paragraph, as created by hitting the “Enter” key, I have something before it and something after it. I now click on a menu somewhere which should put some other node in place of that paragraph.

The larger problem I have is finding out where I am in the document tree. ‘pm.sel’ tells me the caret position (or selected range) and it gives me the DOM node through the ‘lastHeadNode’ property, but not the ProseMirror node. How can I find the ProseMirror node of where the caret is? Is there a way to obtain the PM node from the DOM node? The end goal is to implement a command that does something with the PM document representation.

Thanks again! Boris

pm.sel is not in the documentation and thus not part of the public API. Use pm.selection, which ensures you’re always looking at an up-to-date value.

You don’t need to have anything to do with the DOM in this case. pm.doc.resolve(pm.selection.head) will give you a ‘resolved position’, which has a parent property pointing at the node that the selection is inside of. You can call resolvedPos.before(resolvedPos.depth) to get a position before that node, which you can pass to setNodeType.

perfect,thanks!

Hi again,

Sorry, I still couldn’t get this to work. But I think for a different reason, so I must provide more context. I have a custom block node type:

export class DiscourseContainerElement extends Block {
	get attrs() {
		return { title: new Attribute({})}
	}
       get contains() { return NodeKind.block }	
}

Then, on a freshly created editor with its default initial paragraph inside, I essentially try the following (I used ‘editor’ instead of ‘pm’ as my ProseMirror editor variable):

let initialParagraph = editor.schema.node("paragraph")
let rp = editor.doc.resolve(editor.selection.head) 
let pos = rp.before(rp.depth)
let title = 'Section 1'
editor.tr.setNodeType(pos,
                            editor.schema.nodes.discourseContainer,
                             { title:title})
            .insert(pos, initialParagraph).apply()

So I want to replace the initial paragraph with my custom ‘discourseContainer’ node and insert a paragraph inside it.

I get an exception when try this:

error.js:13 Uncaught Error: Content can not be wrapped in ancestor discourseContainer
ProseMirrorError	@	error.js:13
TransformError	@	transform.js:28
step	@	transform.js:64
_transform.Transform.setNodeType	@	ancestor.js:276
(anonymous function)	@	index.js:82

Sorry if I’m missing something obvious.

Any clues?

Thanks much for your help! Boris

Since your container has different content than a regular paragraph, you can’t use setNodeType to change its type (that would make the paragraph’s child nodes the content of the container node). You’ll need to wrap the paragraph instead.

editor.tr.wrap(editor.selection.from, editor.selection.to,
               editor.schema.nodes.discourseContainer, {title}).apply()

Hi,

Ok, that sort of worked. Except, as soon as I start typing inside that new container, it automatically appends an extra paragraph to the document. It seems to insist that there be a top-level paragraph inside the doc node.

But I don’t understand why can’t I replace the paragraph with something else? I tried ‘setNodeType’ is per your suggestion as a way to replace the node with another one by changing its type. What I really want to do is replace, not wrap the content. I can do:

editor.tr.replaceWith(pos, pos, editor.schema.node(“discourseContainer”, { title:title}, initialParagraph)).apply()

But that initial paragraph remains preserved appended in the doc after my discourseContainer node. Moreover, as I start typing inside my discourseContainer node (specifically inside its nested newly created paragraph), it appends yet another paragraph to the doc. So I end up with 3 paragraphs total. I try to position the caret inside my new paragraph with:

editor.setSelection(new selectionModel.TextSelection(pos + 1, pos + 1))

And the caret is there and I can start typing text. But for some reason the framework decide to append a 3d paragraph to the doc (sort of like with the tr.wrap I mentioned above).

Boris

I can’t reproduce that when I do this with a blockquote, which has pretty much the same semantics as your node. Can you? Want to submit a minimal test case?

You can, as you found out, but you’re not replacing the paragraph, you’re replacing nothing (the start and end are the same), so you’re just inserting another element in front of the paragraph. (This is not the DOM, where using a node in another context automatically removes it from its old context.)

Here is an example to reproduce.

play.js:

import {Inline, Block, Attribute, Schema, defaultSchema, NodeKind} from "prosemirror/dist/model"
import {elt} from "prosemirror/dist/dom"
import {TextSelection} from "prosemirror/dist/edit"

global.prosemirror = require('prosemirror');
global.selectionModel = require('prosemirror/dist/edit/selection.js'
  )
export class DiscourseContainerElement extends Block {
  get attrs() { return { title: new Attribute({})} }
  get contains() { return NodeKind.block }  
}
DiscourseContainerElement.prototype.serializeDOM = (node, s) => {
  let content = s.renderAs(node, 'div');
  return elt('div', {}, elt('b',{}, node.attrs.title), content);
}
global.customProseMirrorSchema = new Schema(defaultSchema.spec.update({discourseContainer: DiscourseContainerElement }))

and the HTML, assuming the above file was browserified into playbundle.js:

<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>ProseMirror demo page</title>
<link rel=stylesheet href="application.css">
</head>
<body>

<button onclick="insertElement()">Go</button>
<div id="editable" contenteditable="true"></div>

<script src="playbundle.js"></script>

<script>
var editor = new prosemirror.ProseMirror({
  place: document.getElementById('editable'),
  schema: customProseMirrorSchema
});
function insertElement() {
  var initialParagraph = editor.schema.node("paragraph");
  var rp = editor.doc.resolve(editor.selection.head);
  var pos = rp.before(rp.depth);
  editor.tr.replaceWith(pos, pos,
    editor.schema.node("discourseContainer", { title:'title' }, initialParagraph)).apply();
  editor.setSelection(new selectionModel.TextSelection(pos, pos));   
 // get back into the editable after button click, in my code is actually positions correctly
// into the new paragraph, but in this example it decides to put the caret into the next paragraph (the one remaining from the initial doc), no clue why that is
  document.getElementById('editable').focus();  
  editor.setSelection(new selectionModel.TextSelection(pos, pos)); 
}
</script>

</body>
</html>

As you can probably guess, my goal is to have some visual container where I can control the outside appearance, e.g. by putting a title that is not really part of the editor.

Ok, well yes, I see that I’m not replacing the paragraph :smile: My question is how to do it? I’ve tried to play with start/end positions, hard-coding to 0 and 1 for example, or 1 and 1, I just have no clue what the rules are. Let me try to state the problem again: there is a blank/empty paragraph somewhere in the doc and I’d like to replace it with something of my own that is a block container and that has its own rendering.

I’m not sure how to insert HTML into this editor, it seems like it doesn’t know how to escape it, so I’ll just paste the JavaScript part that responds to a button click to do the replacement on a blank/empty editable div:

var editor = new prosemirror.ProseMirror({
  place: document.getElementById('editable'),
  schema: customProseMirrorSchema
});
function insertElement() {
  var initialParagraph = editor.schema.node("paragraph");
  var rp = editor.doc.resolve(editor.selection.head);
  var pos = rp.before(rp.depth);
  editor.tr.replaceWith(pos, pos ,
    editor.schema.node("discourseContainer", { title:'title' }, initialParagraph)).apply();
  editor.setSelection(new selectionModel.TextSelection(pos, pos));   
  document.getElementById('editable').focus();  
  editor.setSelection(new selectionModel.TextSelection(pos, pos)); 
}

Use triple backticks around code blocks to get them formatted properly and not escaped/stripped.

To replace node node at position pos, tr.replaceWith(pos, pos + node.nodeSize, ...) should work. It seems like reading the doc guide might help get a mental model of what is going on.

Ok, fixed!

I had read the docs, but I hadn’t noticed the updated section on Indexing with the new positions. And I’ve been trying to read the implementation, but it will take me more time to get familiar with the code, especially since I haven’t worked that much with JavaScript.

Ok, doing:

let rp = editor.doc.resolve(editor.selection.head) 
let pos =   rp.before(rp.depth)
editor.tr.replaceWith(pos, pos + rp.parent.nodeSize, etc...

does get rid of that paragraph!

To solve the more subtle problem of it creating a new paragraph as soon as I start typing, I position the caret at

editor.setSelection(new selectionModel.TextSelection(pos + 2, pos + 2))

that is I’m skipping the discourseContainer token" and the “paragraph token” so I’m at the first character inside the paragraph, right? Otherwise it was confused because it wasn’t properly placed inside a paragraph I suppose.

Thanks much for your patience!

Ah, you had the selection after the new node before. That’s actually not allowed – if you had used setTextSelection to create the selection it would have thrown an error.

If anyone else is facing this issue, the following SCSS will also work:

.ProseMirror {
  p:not(:first-child) {
    display: none;
  }
}