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
|
* The text content of the link node - will be displayed in the drawer
|
||||||
*/
|
*/
|
||||||
text: null | string
|
text: null | string
|
||||||
}
|
} | null
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export const LinkPlugin: PluginComponent<ClientProps> = ({ clientProps }) => {
|
|||||||
editor.registerCommand(
|
editor.registerCommand(
|
||||||
TOGGLE_LINK_COMMAND,
|
TOGGLE_LINK_COMMAND,
|
||||||
(payload: LinkPayload) => {
|
(payload: LinkPayload) => {
|
||||||
|
if (payload === null) {
|
||||||
|
$toggleLink(null)
|
||||||
|
return true
|
||||||
|
}
|
||||||
if (!payload.fields?.linkType) {
|
if (!payload.fields?.linkType) {
|
||||||
payload.fields.linkType = clientProps.defaultLinkType as any
|
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> =
|
export const TOGGLE_LINK_COMMAND: LexicalCommand<LinkPayload | null> =
|
||||||
createCommand('TOGGLE_LINK_COMMAND')
|
createCommand('TOGGLE_LINK_COMMAND')
|
||||||
|
|
||||||
export function $toggleLink(payload: { fields: LinkFields } & LinkPayload): void {
|
export function $toggleLink(payload: ({ fields: LinkFields } & LinkPayload) | null): void {
|
||||||
const selection = $getSelection()
|
const selection = $getSelection()
|
||||||
|
|
||||||
if (!$isRangeSelection(selection) && !payload.selectedNodes?.length) {
|
if (!$isRangeSelection(selection) && (payload === null || !payload.selectedNodes?.length)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const nodes = $isRangeSelection(selection) ? selection.extract() : payload.selectedNodes
|
const nodes = $isRangeSelection(selection)
|
||||||
|
? selection.extract()
|
||||||
|
: payload === null
|
||||||
|
? []
|
||||||
|
: payload.selectedNodes
|
||||||
|
|
||||||
if (payload === null) {
|
if (payload === null) {
|
||||||
// Remove LinkNodes
|
// Remove LinkNodes
|
||||||
@@ -289,92 +293,93 @@ export function $toggleLink(payload: { fields: LinkFields } & LinkPayload): void
|
|||||||
parent.remove()
|
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()) {
|
return
|
||||||
// remove all children and add child with new textcontent:
|
}
|
||||||
linkNode.append($createTextNode(payload.text))
|
// Add or merge LinkNodes
|
||||||
linkNode.getChildren().forEach((child) => {
|
if (nodes?.length === 1) {
|
||||||
if (child !== linkNode.getLastChild()) {
|
const firstNode = nodes[0]
|
||||||
child.remove()
|
// 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
|
if ($isLinkNode(node)) {
|
||||||
let linkNode: LinkNode | null = null
|
if (node.is(linkNode)) {
|
||||||
|
|
||||||
nodes?.forEach((node) => {
|
|
||||||
const parent = node.getParent()
|
|
||||||
|
|
||||||
if (parent === linkNode || parent === null || ($isElementNode(node) && !node.isInline())) {
|
|
||||||
return
|
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) {
|
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 {
|
function $getLinkAncestor(node: LexicalNode): LinkNode | null {
|
||||||
|
|||||||
@@ -1218,6 +1218,65 @@ describe('lexicalMain', () => {
|
|||||||
await page.getByText('Creating new User')
|
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', () => {
|
describe('localization', () => {
|
||||||
test.skip('ensure simple localized lexical field works', async () => {
|
test.skip('ensure simple localized lexical field works', async () => {
|
||||||
await navigateToLexicalFields(true, true)
|
await navigateToLexicalFields(true, true)
|
||||||
|
|||||||
Reference in New Issue
Block a user