-
-
Notifications
You must be signed in to change notification settings - Fork 597
feat: enhance text cursor positioning in blocks #1894
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
@must is attempting to deploy a commit to the TypeCell Team on Vercel. A member of the Team first needs to authorize it. |
🔍 Existing Issues For ReviewYour pull request is modifying functions with the following pre-existing issues: 📄 File: packages/core/src/api/blockManipulation/selections/textCursorPosition.ts
Did you find this useful? React with a 👍 or 👎 |
Hi @must, really appreciate you digging into this. While I agree that we need something like this, I don't think that this particular solution is what we are looking for right now. In this PR I laid out some of the things that we want to move toward in future versions of blocknote: #1756 Essentially we are looking to move toward an API that does include character offsets, but in a way that the user doesn't need to specifically always use character offsets. What you've implemented is a good stepping stone towards that sort of an API, but we will need to take this further before introducing more concepts to our existing API. |
Thanks for the thoughtful feedback @nperez0111 . I really appreciate the forward-looking approach here. I just started using BlockNote on a project and it’s awesome. While building with table updates, preserving the cursor position was a blocker which led me to issue #1815 . I’m fully on board with the Location API vision you’ve sketched in the ADR and your plan to evolve the editor’s Transform API. My recent offset-based patch was simply a quick fix to unblock what I needed to build. What I’d love to know is can we land a minimal interim update on main that keeps the cursor position during table block updates? I'm happy to contribute as much as possible but I'm just starting out with this repo so I'm trying to get the least effort highest reward contribution here as I get more familiar with what you've built so far (quite a lot). Let me know what’s reasonable in the short term and how I can help shape the long-term version. |
@must so the root issue you are running into is around preserving the cursor during block updates? I think that is a great place to start and definitely something that we should improve on. Would that unblock you on what you are building? (what are you building, btw, always nice to hear). I can help guide you on where that issue is, if so |
@nperez0111 This is much appreciated! So yeah the root cause that led me down this path is table block updates resulting in the cursor jumping towards the end. Building an LLM-assisted quote-generation tool with real-time, collaborative editing capabilities which requires dynamic updates to table cells while keeping the cursor intact. It's part of Narrative I might still be missing some things here, but any pointers you can share would be super helpful |
@must Looks cool! Which sorts of table block updates you are referring to? |
@nperez0111 thanks! As simple as user makes an input (keystroke) on a cell of the same table and we modify another next to it to reflect some change in calculations then using updateBlock on said table / cell contents results in the cursor jumping to the end of the doc. If I'm not mistaken, I have to update the whole table block as there's no ids on the cells? |
@must yep, you are right. Right now, tables are one of our most complicated blocks since cells act as a sort of block - we are looking to support nested blocks in the future, and tables will likely be one of them. But, for now. I think that it is sufficient to work on the updateBlock API, and trying to preserve the selection if possible. I made an update to a table cell, and narrowed it down to this code which definitely moves the selection. I think that what we will probably want to do here is to:
Once we have this in-place, you should be able to update the content of a table and the selection will be preserved where it was. |
@nperez0111 thanks for the pointers. And sorry for the delay. I tried a few approaches to preserve the cursor. The real blocker is that table cells don’t have IDs. This is especially problematic when multiple cells change and the cursor is on a subsequent cell. Previous character changes needs to be accounted for then otherwise the selection would jump by the previous changes). I've pushed an example that does not rely on cell ids and this still presents the aforementioned issue. I'd imagine nested blocks for tables is the approach you were referring to here? We could hack around it with deltas, but that feels brittle. Easiest way seems to add ids to cells so we can restore via cell id and localOffset. What do you think? |
I will take a look at the specifics later but yes I referring to nested blocks where tables could be a container and the cells have IDs but we can't support that right now (and it would be a lot of work to do so) I would suggest doing something like figuring out which cell within the table the selection is within (row and column) and move the selection back to that cell if possible (i.e. the row and column still exist). Then from there character offsets will be able to restore it better once they are implemented |
@nperez0111 I just changed the approach to reflect your last comment. It's now working quite well and should resolve the issues I had including the in-cell cursor preservation which is great! Let me know if you have any actionable feedback to get this through |
@must this looks great in principle. I think I want to spend some time to simplify the cell anchor stuff, but it is mostly there. So you can use this in your project, I'll need you to get the tests to pass so that the build can make a pkg.pr.new version for you. I think this may need to be all you need to apply, for some reason I can't stack commits on your fork: diff --git c/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts w/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts
index f4e99a572..f7eb2ea9a 100644
--- c/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts
+++ w/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts
@@ -350,7 +350,9 @@ type CellAnchor = { row: number; col: number; offset: number };
*/
export function captureCellAnchor(tr: Transaction): CellAnchor | null {
const sel = tr.selection;
- if (!(sel instanceof TextSelection)) return null;
+ if (!(sel instanceof TextSelection)) {
+ return null;
+ }
const $head = tr.doc.resolve(sel.head);
// Find enclosing cell and table
@@ -366,19 +368,25 @@ export function captureCellAnchor(tr: Transaction): CellAnchor | null {
break;
}
}
- if (cellDepth < 0 || tableDepth < 0) return null;
+ if (cellDepth < 0 || tableDepth < 0) {
+ return null;
+ }
// Absolute positions (before the cell)
const cellPos = $head.before(cellDepth);
const tablePos = $head.before(tableDepth);
const table = tr.doc.nodeAt(tablePos);
- if (!table || table.type.name !== "table") return null;
+ if (!table || table.type.name !== "table") {
+ return null;
+ }
// Visual grid position via TableMap (handles spans)
const map = TableMap.get(table);
const rel = cellPos - (tablePos + 1); // relative to inside table
const idx = map.map.indexOf(rel);
- if (idx < 0) return null;
+ if (idx < 0) {
+ return null;
+ }
const row = Math.floor(idx / map.width);
const col = idx % map.width;
@@ -396,7 +404,9 @@ function restoreCellAnchor(
blockInfo: BlockInfo,
a: CellAnchor,
): boolean {
- if (blockInfo.blockNoteType !== "table") return false;
+ if (blockInfo.blockNoteType !== "table") {
+ return false;
+ }
// 1) Resolve the table node in the current document
let tablePos = -1;
@@ -418,7 +428,9 @@ function restoreCellAnchor(
}
const table = tablePos >= 0 ? tr.doc.nodeAt(tablePos) : null;
- if (!table || table.type.name !== "table") return false;
+ if (!table || table.type.name !== "table") {
+ return false;
+ }
// 2) Clamp row/col to the table’s current grid
const map = TableMap.get(table);
@@ -428,7 +440,9 @@ function restoreCellAnchor(
// 3) Compute the absolute position of the target cell (pos BEFORE the cell)
const cellIndex = row * map.width + col;
const relCellPos = map.map[cellIndex]; // relative to (tablePos + 1)
- if (relCellPos == null) return false;
+ if (relCellPos == null) {
+ return false;
+ }
const cellPos = tablePos + 1 + relCellPos;
// 4) Place the caret inside the cell, clamping the text offset |
setTextCursorPosition
to accept numeric offsets, allowing precise cursor placement.