fix(richtext-lexical): error when deleting links (#10557)
When pressing the delete button in the floating link popup, the link was not deleted and a console error was thrown
This commit is contained in:
@@ -16,4 +16,4 @@ export type LinkPayload = {
|
||||
* The text content of the link node - will be displayed in the drawer
|
||||
*/
|
||||
text: null | string
|
||||
}
|
||||
} | null
|
||||
|
||||
@@ -29,6 +29,10 @@ export const LinkPlugin: PluginComponent<ClientProps> = ({ clientProps }) => {
|
||||
editor.registerCommand(
|
||||
TOGGLE_LINK_COMMAND,
|
||||
(payload: LinkPayload) => {
|
||||
if (payload === null) {
|
||||
$toggleLink(null)
|
||||
return true
|
||||
}
|
||||
if (!payload.fields?.linkType) {
|
||||
payload.fields.linkType = clientProps.defaultLinkType as any
|
||||
}
|
||||
|
||||
@@ -266,13 +266,17 @@ export function $isLinkNode(node: LexicalNode | null | undefined): node is LinkN
|
||||
export const TOGGLE_LINK_COMMAND: LexicalCommand<LinkPayload | null> =
|
||||
createCommand('TOGGLE_LINK_COMMAND')
|
||||
|
||||
export function $toggleLink(payload: { fields: LinkFields } & LinkPayload): void {
|
||||
export function $toggleLink(payload: ({ fields: LinkFields } & LinkPayload) | null): void {
|
||||
const selection = $getSelection()
|
||||
|
||||
if (!$isRangeSelection(selection) && !payload.selectedNodes?.length) {
|
||||
if (!$isRangeSelection(selection) && (payload === null || !payload.selectedNodes?.length)) {
|
||||
return
|
||||
}
|
||||
const nodes = $isRangeSelection(selection) ? selection.extract() : payload.selectedNodes
|
||||
const nodes = $isRangeSelection(selection)
|
||||
? selection.extract()
|
||||
: payload === null
|
||||
? []
|
||||
: payload.selectedNodes
|
||||
|
||||
if (payload === null) {
|
||||
// Remove LinkNodes
|
||||
@@ -289,92 +293,93 @@ export function $toggleLink(payload: { fields: LinkFields } & LinkPayload): void
|
||||
parent.remove()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Add or merge LinkNodes
|
||||
if (nodes?.length === 1) {
|
||||
const firstNode = nodes[0]
|
||||
// if the first node is a LinkNode or if its
|
||||
// parent is a LinkNode, we update the URL, target and rel.
|
||||
const linkNode: LinkNode | null = $isLinkNode(firstNode)
|
||||
? firstNode
|
||||
: $getLinkAncestor(firstNode)
|
||||
if (linkNode !== null) {
|
||||
linkNode.setFields(payload.fields)
|
||||
|
||||
if (payload.text != null && payload.text !== linkNode.getTextContent()) {
|
||||
// remove all children and add child with new textcontent:
|
||||
linkNode.append($createTextNode(payload.text))
|
||||
linkNode.getChildren().forEach((child) => {
|
||||
if (child !== linkNode.getLastChild()) {
|
||||
child.remove()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
// Add or merge LinkNodes
|
||||
if (nodes?.length === 1) {
|
||||
const firstNode = nodes[0]
|
||||
// if the first node is a LinkNode or if its
|
||||
// parent is a LinkNode, we update the URL, target and rel.
|
||||
const linkNode: LinkNode | null = $isLinkNode(firstNode)
|
||||
? firstNode
|
||||
: $getLinkAncestor(firstNode)
|
||||
if (linkNode !== null) {
|
||||
linkNode.setFields(payload.fields)
|
||||
|
||||
if (payload.text != null && payload.text !== linkNode.getTextContent()) {
|
||||
// remove all children and add child with new textcontent:
|
||||
linkNode.append($createTextNode(payload.text))
|
||||
linkNode.getChildren().forEach((child) => {
|
||||
if (child !== linkNode.getLastChild()) {
|
||||
child.remove()
|
||||
}
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let prevParent: ElementNodeType | LinkNode | null = null
|
||||
let linkNode: LinkNode | null = null
|
||||
|
||||
nodes?.forEach((node) => {
|
||||
const parent = node.getParent()
|
||||
|
||||
if (parent === linkNode || parent === null || ($isElementNode(node) && !node.isInline())) {
|
||||
return
|
||||
}
|
||||
|
||||
if ($isLinkNode(parent)) {
|
||||
linkNode = parent
|
||||
parent.setFields(payload.fields)
|
||||
if (payload.text != null && payload.text !== parent.getTextContent()) {
|
||||
// remove all children and add child with new textcontent:
|
||||
parent.append($createTextNode(payload.text))
|
||||
parent.getChildren().forEach((child) => {
|
||||
if (child !== parent.getLastChild()) {
|
||||
child.remove()
|
||||
}
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!parent.is(prevParent)) {
|
||||
prevParent = parent
|
||||
linkNode = $createLinkNode({ fields: payload.fields })
|
||||
|
||||
if ($isLinkNode(parent)) {
|
||||
if (node.getPreviousSibling() === null) {
|
||||
parent.insertBefore(linkNode)
|
||||
} else {
|
||||
parent.insertAfter(linkNode)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
node.insertBefore(linkNode)
|
||||
}
|
||||
}
|
||||
|
||||
let prevParent: ElementNodeType | LinkNode | null = null
|
||||
let linkNode: LinkNode | null = null
|
||||
|
||||
nodes?.forEach((node) => {
|
||||
const parent = node.getParent()
|
||||
|
||||
if (parent === linkNode || parent === null || ($isElementNode(node) && !node.isInline())) {
|
||||
if ($isLinkNode(node)) {
|
||||
if (node.is(linkNode)) {
|
||||
return
|
||||
}
|
||||
|
||||
if ($isLinkNode(parent)) {
|
||||
linkNode = parent
|
||||
parent.setFields(payload.fields)
|
||||
if (payload.text != null && payload.text !== parent.getTextContent()) {
|
||||
// remove all children and add child with new textcontent:
|
||||
parent.append($createTextNode(payload.text))
|
||||
parent.getChildren().forEach((child) => {
|
||||
if (child !== parent.getLastChild()) {
|
||||
child.remove()
|
||||
}
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!parent.is(prevParent)) {
|
||||
prevParent = parent
|
||||
linkNode = $createLinkNode({ fields: payload.fields })
|
||||
|
||||
if ($isLinkNode(parent)) {
|
||||
if (node.getPreviousSibling() === null) {
|
||||
parent.insertBefore(linkNode)
|
||||
} else {
|
||||
parent.insertAfter(linkNode)
|
||||
}
|
||||
} else {
|
||||
node.insertBefore(linkNode)
|
||||
}
|
||||
}
|
||||
|
||||
if ($isLinkNode(node)) {
|
||||
if (node.is(linkNode)) {
|
||||
return
|
||||
}
|
||||
if (linkNode !== null) {
|
||||
const children = node.getChildren()
|
||||
|
||||
for (let i = 0; i < children.length; i += 1) {
|
||||
linkNode.append(children[i])
|
||||
}
|
||||
}
|
||||
|
||||
node.remove()
|
||||
return
|
||||
}
|
||||
|
||||
if (linkNode !== null) {
|
||||
linkNode.append(node)
|
||||
const children = node.getChildren()
|
||||
|
||||
for (let i = 0; i < children.length; i += 1) {
|
||||
linkNode.append(children[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
node.remove()
|
||||
return
|
||||
}
|
||||
|
||||
if (linkNode !== null) {
|
||||
linkNode.append(node)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function $getLinkAncestor(node: LexicalNode): LinkNode | null {
|
||||
|
||||
@@ -1218,6 +1218,65 @@ describe('lexicalMain', () => {
|
||||
await page.getByText('Creating new User')
|
||||
})
|
||||
|
||||
test('ensure links can created from clipboard and deleted', async () => {
|
||||
await navigateToLexicalFields()
|
||||
const richTextField = page.locator('.rich-text-lexical').first()
|
||||
await richTextField.scrollIntoViewIfNeeded()
|
||||
await expect(richTextField).toBeVisible()
|
||||
// Wait until there at least 10 blocks visible in that richtext field - thus wait for it to be fully loaded
|
||||
await expect(page.locator('.rich-text-lexical').nth(2).locator('.lexical-block')).toHaveCount(
|
||||
10,
|
||||
)
|
||||
await expect(page.locator('.shimmer-effect')).toHaveCount(0)
|
||||
await richTextField.locator('.ContentEditable__root').first().click()
|
||||
const lastParagraph = richTextField.locator('p').first()
|
||||
await lastParagraph.scrollIntoViewIfNeeded()
|
||||
await expect(lastParagraph).toBeVisible()
|
||||
|
||||
await lastParagraph.click()
|
||||
|
||||
page.context().grantPermissions(['clipboard-read', 'clipboard-write'])
|
||||
|
||||
// Paste in a link copied from a html page
|
||||
const link = '<a href="https://www.google.com">Google</a>'
|
||||
await page.evaluate(
|
||||
async ([link]) => {
|
||||
const blob = new Blob([link], { type: 'text/html' })
|
||||
const clipboardItem = new ClipboardItem({ 'text/html': blob })
|
||||
await navigator.clipboard.write([clipboardItem])
|
||||
},
|
||||
[link],
|
||||
)
|
||||
|
||||
await page.keyboard.press('Meta+v')
|
||||
await page.keyboard.press('Control+v')
|
||||
|
||||
const linkNode = richTextField.locator('a.LexicalEditorTheme__link').first()
|
||||
await linkNode.scrollIntoViewIfNeeded()
|
||||
await expect(linkNode).toBeVisible()
|
||||
|
||||
// Check link node text and attributes
|
||||
await expect(linkNode).toHaveText('Google')
|
||||
await expect(linkNode).toHaveAttribute('href', 'https://www.google.com/')
|
||||
|
||||
// Expect floating link editor link-input to be there
|
||||
const linkInput = richTextField.locator('.link-input').first()
|
||||
await expect(linkInput).toBeVisible()
|
||||
|
||||
const linkInInput = linkInput.locator('a').first()
|
||||
expect(linkInInput).toBeVisible()
|
||||
|
||||
expect(linkInInput).toContainText('https://www.google.com/')
|
||||
await expect(linkInInput).toHaveAttribute('href', 'https://www.google.com/')
|
||||
|
||||
// Click remove button
|
||||
const removeButton = linkInput.locator('.link-trash').first()
|
||||
await removeButton.click()
|
||||
|
||||
// Expect link to be removed
|
||||
await expect(linkNode).not.toBeVisible()
|
||||
})
|
||||
|
||||
describe('localization', () => {
|
||||
test.skip('ensure simple localized lexical field works', async () => {
|
||||
await navigateToLexicalFields(true, true)
|
||||
|
||||
Reference in New Issue
Block a user