-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Fix insertNodes
and insertParagraph
#5002
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
insertNewAfter( | ||
selection: RangeSelection, | ||
_: RangeSelection, | ||
restoreSelection = true, | ||
): null | ElementNode { | ||
const element = this.getParentOrThrow().insertNewAfter( | ||
selection, | ||
restoreSelection, | ||
); | ||
if ($isElementNode(element)) { | ||
const linkNode = $createLinkNode(this.__url, { | ||
rel: this.__rel, | ||
target: this.__target, | ||
title: this.__title, | ||
}); | ||
element.append(linkNode); | ||
return linkNode; | ||
} | ||
return null; | ||
const linkNode = $createLinkNode(this.__url, { | ||
rel: this.__rel, | ||
target: this.__target, | ||
title: this.__title, | ||
}); | ||
this.insertAfter(linkNode, restoreSelection); | ||
return linkNode; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quoting from the PR description:
Due to design errors in those methods that were modified, insertNewAfter in LinkNode was not "inserting a new link after", but rather a new block. This hack has been fixed and removed as it is no longer needed. I've also added a to-do comment that insertNewAfter should not return LexicalNode | null but this | null. This would be easier to achieve now.
const linkNode = $isLinkNode(firstNode) | ||
? firstNode | ||
: $getLinkAncestor(firstNode); | ||
const linkNode = $getAncestor(firstNode, $isLinkNode); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One detail that I noticed the new $getAncestor
function improved
const firstSelectedBlock = $getAncestor( | ||
selection.anchor.getNode(), | ||
INTERNAL_$isBlock, | ||
); | ||
if (firstSelectedBlock && nodes.indexOf(firstSelectedBlock) === -1) { | ||
nodes.push(firstSelectedBlock); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved $isBlock from this file. I remembered that a few lines above (here), I had this code where I searched for the closest block ancestor. Cleaner this way.
insertRawText(text: string): void { | ||
const parts = text.split(/(\r?\n|\t)/); | ||
const nodes = []; | ||
const paragraph = $createParagraphNode(); | ||
const length = parts.length; | ||
for (let i = 0; i < length; i++) { | ||
const part = parts[i]; | ||
if (part === '\n' || part === '\r\n') { | ||
nodes.push($createLineBreakNode()); | ||
paragraph.append($createLineBreakNode()); | ||
} else if (part === '\t') { | ||
nodes.push($createTabNode()); | ||
paragraph.append($createTabNode()); | ||
} else { | ||
nodes.push($createTextNode(part)); | ||
paragraph.append($createTextNode(part)); | ||
} | ||
} | ||
this.insertNodes(nodes); | ||
this.insertNodes([paragraph]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is required by modifications to the insertNodes
method
function splitBlock(point: PointType, selection: RangeSelection) { | ||
let pointNode = point.getNode(); | ||
if ($isElementNode(pointNode)) { | ||
// TO-DO: InsertNewAfter should return 'null | this' instead of 'null | LexicalNode' | ||
const newBlock = pointNode.insertNewAfter(selection, false) as ElementNode; | ||
if (newBlock) newBlock.select(); | ||
return newBlock; | ||
} | ||
|
||
const splitElement = (element: ElementNode) => { | ||
const {offset} = point; | ||
const parent = pointNode.getParentOrThrow(); | ||
const x = parent.isInline() ? parent : pointNode; | ||
const firstToAppend = offset === 0 ? x : pointNode.splitText(offset)[0]; | ||
const siblings = firstToAppend.getNextSiblings(); | ||
const nodesToAppend = | ||
offset === 0 ? [firstToAppend, ...siblings] : siblings; | ||
const newElement = element.insertNewAfter(selection, false) as ElementNode; | ||
if (newElement) { | ||
newElement.append(...nodesToAppend); | ||
newElement.selectStart(); | ||
} | ||
return newElement; | ||
}; | ||
|
||
if (pointNode.getParentOrThrow().isInline()) { | ||
splitElement(pointNode.getParentOrThrow()); | ||
} | ||
|
||
pointNode = point.getNode(); | ||
const block = $getAncestor(pointNode, INTERNAL_$isBlock); | ||
if (block) return splitElement(block); | ||
return null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically the splitElement function is executed once, unless the anchor is inside an inline element (in which case it is executed twice).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will it handle case when link is inside mark node (inline within inline)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. No. But right now it's not working on Lexical either. You can confirm it in the playground by pressing enter on a comment that involves a link. That's why it should be a job for another PR.
focusOffset: 16, | ||
focusPath: [2, 0, 0], | ||
focusPath: [1, 0, 0], | ||
}); | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be considered a bug fixed, since there is no need to insert an empty paragraph
</ul> | ||
<hr class="" contenteditable="false" data-lexical-decorator="true" /> | ||
<ul class="PlaygroundEditorTheme__ul"> | ||
<li | ||
value="2" | ||
class="PlaygroundEditorTheme__listItem PlaygroundEditorTheme__ltr" | ||
dir="ltr"> | ||
dir="ltr" | ||
value="1"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another improvement to insertNodes.
It doesn't make sense for the hr to be inserted after the list-item the cursor is over, if the cursor is at the beginning.
Ok I did it! Additionally, this PR lays the groundwork for greatly simplifying |
if ('__language' in firstBlock) { | ||
if ('__language' in nodes[0]) { | ||
this.insertText(nodes[0].getTextContent()); | ||
} else { | ||
target = placementNode; | ||
const index = RemoveTextAndSplitBlock(this); | ||
firstBlock.splice(index, 0, nodes); | ||
const last = nodes.at(-1)!; | ||
if (last.select) last.select(); | ||
else last.selectNext(0, 0); | ||
} | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The node insertion logic is different in code.
I used '__language' in node
instead of isCodeNode(node)
to prevent core from depending on @lexical/code. Something that may be more elegant is to create an INSERT_NODES_COMMAND
to which lexical/code
is hooked.
I am going to address this at #4900
Tried this out locally, looks like it also fixes #5088 |
Thanks @GermanJablo, appreciate your hard work on this! |
I'm glad to be of help! I would appreciate any priority that can be given to this PR since keeping it updated with main can be difficult. If there's anything else I can do to move this forward, just let me know. Thank you! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, great work GermanJablo! Left some minor comments
const split = firstSelectionNode.splitText(anchor.offset)[0]; | ||
const x = anchor.offset === 0 ? 0 : 1; | ||
const index = split.getIndexWithinParent() + x; | ||
const codeNode = firstSelectionNode.getParentOrThrow(); | ||
const nodesToInsert = [$createLineBreakNode(), ...insertNodes]; | ||
codeNode.splice(index, 0, nodesToInsert); | ||
const last = insertNodes.at(-1); | ||
if (last) last.select(); | ||
else if (anchor.offset === 0) split.selectPrevious(); | ||
else split.getNextSibling()!.selectNext(0, 0); | ||
} | ||
if ($isCodeNode(firstSelectionNode)) { | ||
const {offset} = selection.anchor; | ||
firstSelectionNode.splice(offset, 0, [$createLineBreakNode()]); | ||
firstSelectionNode.select(offset + 1, offset + 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So add what point would you draw the line between using insertNodes
and having to handle it yourself? My first impression on the above code is that you're repeating the TextNode split logic present in insertNodes
and insertText
.
Also, this logic is particularly complicated for anyone else to replicate, is there a possibility to have utilities for this?
One of the reasons why users use insertNodes
heavily is to avoid having to split nodes at selection (otherwise insertAfter
would do)
packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/CopyAndPaste.spec.mjs
Outdated
Show resolved
Hide resolved
anchorOffet === this.getTextContentSize() || !selection | ||
? $createParagraphNode() | ||
: $createHeadingNode(this.getTag()); | ||
const direction = this.getDirection(); | ||
newElement.setDirection(direction); | ||
this.insertAfter(newElement, restoreSelection); | ||
if (anchorOffet === 0 && !this.isEmpty() && selection) { | ||
this.replace($createParagraphNode(), restoreSelection); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thoughts as above even though in this case the resulting code is much simpler
packages/lexical-selection/src/__tests__/unit/LexicalSelectionHelpers.test.ts
Show resolved
Hide resolved
): TextNode { | ||
invariant(false, 'TabNode does not support spliceText'); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to remove this? Basically TabNode is a one-off node that it's __text
should never be modified, it should be a static \t
. Other text should be preprended or inserted after, this node can only render \t
Thanks to you @zurfyx. I already answered/resolved the comments! |
This restores the behavior that changed in facebook#5002 particularly when pasting into links and other inline elements. Fixes facebook#5251.
This restores the behavior that changed in facebook#5002 particularly when pasting into links and other inline elements. Fixes facebook#5251.
This restores the behavior that changed in facebook#5002 particularly when pasting into links and other inline elements. Fixes facebook#5251.
This restores the behavior that changed in facebook#5002 particularly when pasting into links and other inline elements. Fixes facebook#5251.
This restores the behavior that changed in facebook#5002 particularly when pasting into links and other inline elements. Fixes facebook#5251.
This restores the behavior that changed in facebook#5002 particularly when pasting into links and other inline elements. Fixes facebook#5251.
insertNodes
andinsertParagraph
methods and resolves Bug: Pasting text immediately after an inline element (e.g. link) causes the text and its following siblings to be moved into the inline element #4295insertNewAfter
inLinkNode
was not "inserting a new link after", but rather a new block. This hack has been fixed and removed as it is no longer needed.I've also added a to-do comment that. Change of mind. Actually what would have been better is forinsertNewAfter
should not returnLexicalNode | null
butthis | null
. This would be easier to achieve nowinsertNewAfter
to be calledhandleEnter
, since it is used only for that, and leads to confusion.$isBlock
function fromrange-selection.ts
to@lexical/utils
. I've marked it as internal use because I think maybe later some nodes should expose an isBlock() method. Topic for another PR.$getAncestor
function fromlexical-link/src/index.ts
to@lexical/utils
. I have corrected a minor error that this function had and have also made it return a more precise type. I have used this function in a couple of places where you can see how it simplifies the code. Surely there is potential to apply it in many more places. A tip: often when using thenode.isInline
method what you are actually trying to achieve is$getAncestor(node, $isBlock)
.