Is there a recommended way of controlling where the gapcursor goes? I have a set document structure like this:
doc(article(title(...),...,body(...)))
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:
doc(article(title(...),...,body(table(...)))|)
Rather than, as I would expect, after the table:
doc(article(title(...),...,body(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.
Itâll be put at the outermost (least deep) position. Having multiple gap cursor positions that are visually identical would be confusing, so this is how it determines a canonical position.
Thatâs very odd. Are the any errors in your dev console when this 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? - #13 by Jordan). 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.
And that should be okay, since itâll search for a valid insert position when you actually insert something.
The replace algorithm will automatically wrap such content so that it fits, if youâve structured your schema so that paragraphs are the first block-like node, this will just work as it is.
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.
Btw - While writing my patch, I made some basic errors the first few times with undefined variables in the replace-function, which ended up throwing JavaScript errors. The odd thing about this was that when those errors were thrown, the new paragraph with content in it was actually inserted in the correct place (the code ensuring correct gapcursor placement had been added earlier). I figure if that function simply fails, it falls back to some generic insertion function that then can insert content correctly.
The idea is that this is okay, since it will search for a valid insertion point when you insert something.
Your best bet at this point is to try and find the minimal schema that illustrates the problem, and submit a test file with that, so that I can see whatâs going on.
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.
I suspect there is something about replaceStep that isnât quite working right, or maybe itâs not being called entirely correctly or some such thing.
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.