Is there a recommended way of controlling where the gapcursor goes? I have a set document structure like this:
If there is only a table in the body and I move the cursor with the arrow keys to the right from the last table cell, the gap caret blinks horizontally and is being put after the article node:
Rather than, as I would expect, after the table:
If the user inputs any keys with the gap cursor there, the input goes into the last table cell rather than starting a new paragraph after the table. If I hit “Enter”, I would expect for a new, empty paragraph to be formed after the table, but nothing happens.
Ok, the thing is that if it weren’t for this gapcursor, there really is no other way for the user to put the caret that far out. The selection is normally always within the article node (likely with the exceptions mentioned in Changing doc.attrs?). This is a minor issue, but with the text being added where it is, it’s not really useful for me right now.
No, and it does not seem that strange to me. The scheme does not allow anything except one article node in the document node, and it only allows a predetermined list of children in the article node. So it cannot add text in any of those. What I would like it to do is add a paragraph after the table in the body. But instead it decided to add extra text within the table.
That is, indeed, the idea of the gap cursor – allowing selections where it wasn’t possible before.
Well, let me assure you that it is strange, since the cursor position was outside of the table when you inserted the content. I’ve tried to imitate your schema to reproduce this, and what I got was that the first character I typed was swallowed because it couldn’t be inserted at the cursor position, after which the cursor was reset to the end of the table, causing further typing to end up in the table.
I’ve added a patch to the gap cursor repository that causes a gap cursor selection to search for a nearby position at which the content can be inserted when it can’t be inserted at the cursor position, and I think that will help in your situation.
Ok, good point. Let me rephrase that: The users are only supposed to enter content into one of the “sections” of the document - title, subtitle, abstract, keywords, authors, body. So in the toolbar in which of these five sections the selection currently is (or nothing if the selection crosses part boundaries). I need the gap cursor so that they can enter something beyond a table or figure within the body. But the gap cursor will effectively place the selection outside the body. The solution for our case is simply to treat this position as an exception and for the menu to display that the selection is in the body when it really is not. It shouldn’t matter as any content entered at that place would go into the body.
This is slightly different than what I found, but maybe one of our plugins somehow caused the input event of that first letter to be repeated once. That should then cause the behavior you mention.
I tried this out now. I must be doing something wrong still. I’ve added allowGapCursor: false on the two highest levels (doc and article), hoping the gapcursor would then appear in the third highest level, the body. But instead it does not appear at all beyond the table. Also, I realize I should put it on the table and table row, but given the way prosemirror-tables is structured, I don’t see a way for me to add that using the spec.nodes.append(tableNodes(...)) helper function.
Ok, I’ve tried to modify the gapcursor code so that it would work for us. The two issues seem to be that:
When placing the caret, it always prefers the top level (depth === 0) node, and there is not a way to make it go any lower than that. I have simply made it look for the top most node that does not set allowGapCursor = false.
When inserting at the place of the gapcursor, and that content is inline content and the place the gapcursor is at is not a textblock node, it gives up. Instead, it would be good if it could wrap that inline content in a textblock node, such as a paragraph.
OK, but at least in our case, it always went straight for the top doc node, rather than go right after the table but still within the body node, which is where we needed it to be. From looking at the code, it seems like this was because there was no node beyond the table node inside the body node.
Ok, maybe something is wrong with our scheme, but that didn’t work in our case. It was trying to insert the textnode directly in a block node that wasn’t a textblock node, and that gave it a replacestep with a content.size of 0, which is why it gave upo on that. There are likely better ways to invoke the wrapping than what I have done in the patch though.
Yes, that’s how I understood you earlier. But at least for us, that’s sub-optimal, as the UI shows something different if the selection is in the top node or a node below it. I imagine others will draw borders and padding/margin around some of their sub elements, and then it will be even more obvious that the caret is in the wrong position.
But I understand that this is not the case for the use cases you are looking at, and this is kind of independent from the insertion issue, so whatever solution we come up with for the insertion should work for both approaches.
At this stage I have code that is working for our usecase, but it would be best if we could get this solved in a more general way and with your insights in how everything works, so that this can also applied to other usecases (and I hopefully don’t have to maintain a fork just for our usecase).
Well, I did what you first asked me to do and found the problem. It was trying to insert an inline textnode into a blocknode that could not directly hold textnodes, but needed to wrap these in a textblocknode first.
That might indeed be an issue. But I am not happy with your solution, since it doesn’t address issues like what happens when there are nodes on both sides around a position in a node that disallows gap cursors — two gap cursor positions? only one? which side? — and generally feels a bit ad-hoc.
The intended behavior is that replaceStep will, on not finding a place to directly insert the content, find a way to wrap it (via line 282 in prosemirror-transform/src/replace.js), and automatically do this. I haven’t been able to figure out why this is failing in your case (in my attempt to reproduce the issue it worked). Maybe you could run a similar call (bodyNode.contentMatchAt(bodyNode.childCount).findWrapping(schema.nodes.text)) and see if it returns null and if so, why.