Repository navigation
Upgrade Lexical to 0.51 - #23
Conversation
Bump lexical, @lexical/history and @lexical/utils from ^0.30.0 to ^0.51.0 (peer and dev ranges too) and add @lexical/extension, then adapt to the breaking changes that reach Zettel: - 0.49: node constructors need zero-argument defaults; importJSON of a replaced TextNode now constructs ZettelSpanNode with no arguments. - 0.49: LexicalCommand is invariant; drop explicit registerCommand generics. - 0.47: formatText toggles a range by selection.format; derive it from the selected spans so ranges selected in code toggle off correctly. - 0.45: triple-click normalization left core; reuse the extension's fix. - 0.50: select-all delete leaves a ParagraphNode; transform it into a Zettel text block. - 0.46: stop using the deprecated traversal type parameters. - 0.42: return the root listener's cleanup instead of tracking the previous root, so listeners are also removed on unregister. Lexical still throws when a range from the start of a block ends inside a text run of a later block. Remove non-collapsed ranges with the live selection detached, and cover every editing path in tests and in Chromium. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Wrap each deletion in $editRange instead of a separate $deleteSelection helper, so other editing code can run its own Lexical edit of a range through the same guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 26b1b911ed
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const first = (selection.isBackward() ? selection.focus : selection.anchor).getNode(); | ||
| const format = $isTextNode(first) ? first.getFormat() : selection.format; | ||
| const style = $isTextNode(first) ? first.getStyle() : selection.style; |
There was a problem hiding this comment.
Resolve the first selected character at span boundaries
When a forward range starts at the end of one span and continues into an adjacent span with different formatting, first is still the outgoing span even though it contributes no selected characters. The replacement therefore inherits that span's format/style instead of the first actually selected character—for example, replacing a bold word selected from the end of a preceding plain span inserts plain text. Resolve an endpoint at getTextContentSize() to the next selected text node (and handle the equivalent backward boundary) before saving the replacement formatting.
Useful? React with 👍 / 👎.
Upgrades
@opral/zettel-lexicalfrom Lexical 0.30 to 0.51 (latest).lexical,@lexical/historyand@lexical/utilsmove to^0.51.0in dependencies, peer dependencies and dev dependencies.@lexical/extensionis added. The playground and lix-comments examples uselexical@^0.51.0.The changeset is a major bump for
@opral/zettel-lexical. The new peer range excludes 0.30, so an app has to upgradelexicaltogether with this release. On a^1.xrange it would otherwise pick this up, end up with two Lexical copies and fail at runtime.Lexical breaking changes and how Zettel adapts
I read the release notes for 0.31 through 0.51 and checked each breaking change against the Zettel nodes and plugin.
importJSONgoes through$createfor registered replacements (#9061)textnode constructsnew ZettelSpanNode()with no arguments.fromLexicalStatethrewCannot read properties of undefinedparagraph/textJSON loads, and formats become marksLexicalCommand<T>is invariant (#8877)registerCommand<KeyboardEvent>(KEY_ENTER_COMMAND, …)failed to compile (the payload isKeyboardEvent | null)formatTexttoggles a range byselection.format(#8807)FORMAT_TEXT_COMMANDworks out which formats every selected span has, then aligns to that. Test addedNormalizeTripleClickSelectionExtension(#8520)["XSecond paragraph"]in Chromium)$fixFocusOverselection. Chromium check addedParagraphNode(#8884)<p>that has no Zettel key: a new_keyon every export, and Enter could not split itParagraphNodetransform replaces any Lexical paragraph with a Zettel text block. This also covers inline content that Lexical wraps at the root. Unit and Chromium checks addedgetParent<ZettelTableRowNode>()instanceofnarrowingThe remaining breaking changes don't affect Zettel:
ltr/rtlclasses and$isParentElementRTL(0.35); managed<br data-lexical-managed-linebreak>and the extra trailing<br>inexportDOM(0.46, 0.50). Zettel's HTML comes from@opral/zettel-html, and the Chromium suite passes.NormalizeInlineElementsExtension(0.45). Zettel's image and inline HTML nodes are empty inline elements on purpose.$copyNode/afterCloneFrom(0.33; Zettel nodes copy their state inclone), textFormat/textStyle serialization (0.39),exportJSONviawithField(0.51), named slots, shadow DOM, the clipboardexcludeFromCopychannel,@lexical/code, and React.Cross-block delete bug: still present in 0.51
The bug is in Lexical and still present in 0.51. It reproduces with a plain
ParagraphNode/TextNodeeditor and no Zettel code:|one two/fou|r five six.$getTextNodeOffset: invalid offset 3 for size 1, and the edit does nothing.What happens:
$removeTextFromCaretRangetrims the last text run. It then merges the blocks while the live selection still holds the untrimmed focus offset, andinsertBeforereads that selection.Zettel now does every non-collapsed removal with the live selection detached (
src/selection.ts). The removal rebuilds the selection from its own carets. This is also exported as$removeSelectedText(selection). Typing over a range keeps Lexical's rule that the new text takes the format of the first selected character. Paste removes the range first and then pastes at the caret.src/selection.test.tscovers Backspace, Delete, deleteContentBackward, Alt+Backspace, Cmd+Backspace, typing, Enter, paste, cut and drag-move, with forward and backward selections. Without the guard, 21 of its 22 tests fail. A Chromium check makes a real DOM selection and presses Backspace.Tests
pnpm run cipasses: lint, build, and ast 11, markdown 19, html 17, lexical 59 tests (up from 35).examples/playground,pnpm test) passes 29 checks, 3 of them new: select-all delete leaves an editable Zettel block, a range delete across blocks works, and typing over a triple-click selection keeps the next block.<p>without a Zettel key, the cross-block throw). The triple-click and select-all problems did not happen on 0.30; the cross-block throw did.Compatibility with the open PRs (#18–#22)
I merged this branch into main + #18–#22. After the fixes below,
pnpm run cipasses (lexical 87 tests) and the Chromium playground passes 34 checks.KEY_DOWN_COMMANDlistener atCOMMAND_PRIORITY_EDITOR, and it handles every key. I pushed a2ed350 to Add opt-in Markdown shortcuts to the Lexical plugin #21, which registers the listener atCOMMAND_PRIORITY_LOW. That also works on 0.30.plugin.ts, because both route deletions through a helper. To resolve it, keep Leave and remove lists from the keyboard; never commit an invalid list #20's$deleteSelection(selection, remove, isBackward)and run itsremove(selection)as$editRange(selection, () => remove(selection)). For word and line deletion of a range, passcurrent.isCollapsed() ? current.deleteWord(isBackward) : current.deleteCharacter(isBackward)(and the same withdeleteLine).Not covered here
examples/lix-commentstests fail onmaintoo (lix_create_checkpointnot found, a Lix API change). Only itslexicalrange changed here.markDefs) of the block that is merged in are lost.exportDocumentthen throwsUnresolved mark. This needs its own fix.🤖 Generated with Claude Code