Question: Select the content Copy Paste and Delete in editor doesn't work with error currentNode.extractWithChild is not a function
See original GitHub issueLexical version:0.2.5
Steps To Reproduce
- input some content in editor and submit
- modify the content in Editor which has initialEditorState
- select some content to copy and try to paste
- select some content and try to delete
The current behavior
- copy some contents from out side of editor and paste it in the editor works but once I tried to select the input to copy and paste it it stopped working. I cannot deleted, copy and paste anymore.
error
editState saved in BE
value: "{\"_nodeMap\":[[\"root\",{\"__children\":[\"299\"],\"__dir\":\"ltr\",\"__format\":0,\"__indent\":0,\"__key\":\"root\",\"__parent\":null,\"__type\":\"root\"}],[\"299\",{\"__type\":\"paragraph\",\"__parent\":\"root\",\"__key\":\"299\",\"__children\":[\"325\"],\"__format\":0,\"__indent\":0,\"__dir\":\"ltr\"}],[\"325\",{\"__type\":\"text\",\"__parent\":\"299\",\"__key\":\"325\",\"__text\":\"copy and paste issue example\",\"__format\":0,\"__style\":\"\",\"__mode\":0,\"__detail\":0,\"__marks\":null}]],\"_selection\":{\"anchor\":{\"key\":\"325\",\"offset\":28,\"type\":\"text\"},\"focus\":{\"key\":\"325\",\"offset\":0,\"type\":\"text\"},\"type\":\"range\"}}"
code Editor.tsx
export const customTheme = {
ltr: 'ltr',
rtl: 'rtl',
placeholder: 'editor-placeholder',
paragraph: 'editor-paragraph',
list: {
nested: {
listitem: 'editor-nested-listitem',
},
ol: 'editor-list-ol',
ul: 'editor-list-ul',
listitem: 'editor-listitem',
},
link: 'editor-link',
text: {
bold: 'editor-text-bold',
italic: 'editor-text-italic',
},
};
const useStyles = makeStyles((theme) => ({
editorContainer: {
borderRadius: '2px',
fontWeight: 400,
textAlign: 'left',
borderTopLeftRadius: '10px',
borderTopRightRadius: '10px',
border: `2px solid ${theme.palette.grey[300]}`,
maxWidth: '550px',
},
editorInner: {
background: '#fff',
position: 'relative',
},
editorInput: {
minHeight: '184px',
maxHeight: '2500px',
resize: 'none',
fontSize: '1rem',
position: 'relative',
tabSize: '1',
outline: 0,
padding: theme.spacing(2, 1.5),
overflowY: 'scroll',
},
editor: {
paddingTop: theme.spacing(1),
},
}));
interface Error {
name: string;
message: string;
stack?: string;
}
interface Props {
onChange: (editorState: EditorState, editor: LexicalEditor) => void;
initialValue: EditorState | undefined;
}
const editorConfig = {
// The editor theme
theme: customTheme,
// Handling of errors during update
onError(error: Error) {
throw error;
},
// Any custom nodes go here
nodes: [ListNode, ListItemNode, AutoLinkNode, LinkNode],
};
const Editor = ({ onChange, initialValue }: Props): JSX.Element => {
const classes = useStyles();
return (
<Grid container>
<Grid item xs={12} className={classes.editor}>
<LexicalComposer initialConfig={editorConfig}>
<div className={classes.editorContainer}>
<ToolbarPlugin />
<div className="editor-inner">
<RichTextPlugin
contentEditable={
<ContentEditable
className={classes.editorInput}
/>
}
initialEditorState={
initialValue ? initialValue : null
}
placeholder={null}
/>
<OnChangePlugin onChange={onChange} />
<AutoFocusPlugin />
<ListPlugin />
<LinkPlugin />
</div>
</div>
</LexicalComposer>
</Grid>
</Grid>
);
};
ToolbarPlugin.js https://codesandbox.io/s/lexical-rich-text-example-5tncvy?file=/src/plugins/ToolbarPlugin.js mostly same as this example. thank you for making this
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import `{`
SELECTION_CHANGE_COMMAND,
FORMAT_TEXT_COMMAND,
FORMAT_ELEMENT_COMMAND,
$getSelection,
$isRangeSelection,
INDENT_CONTENT_COMMAND,
OUTDENT_CONTENT_COMMAND,
} from 'lexical';
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
import { $isAtNodeEnd } from '@lexical/selection';
import { $getNearestNodeOfType, mergeRegister } from '@lexical/utils';
import {
INSERT_ORDERED_LIST_COMMAND,
INSERT_UNORDERED_LIST_COMMAND,
REMOVE_LIST_COMMAND,
$isListNode,
ListNode,
} from '@lexical/list';
import { createPortal } from 'react-dom';
import { $isHeadingNode } from '@lexical/rich-text';
import './style.css';
import { Grid } from '@mui/material';
import LinkIcon from '@mui/icons-material/Link';
import FormatAlignJustifyIcon from '@mui/icons-material/FormatAlignJustify';
import FormatAlignLeftIcon from '@mui/icons-material/FormatAlignLeft';
import FormatAlignRightIcon from '@mui/icons-material/FormatAlignRight';
import FormatAlignCenterIcon from '@mui/icons-material/FormatAlignCenter';
import FormatItalicIcon from '@mui/icons-material/FormatItalic';
import FormatBoldIcon from '@mui/icons-material/FormatBold';
import FormatIndentDecreaseIcon from '@mui/icons-material/FormatIndentDecrease';
import FormatIndentIncreaseIcon from '@mui/icons-material/FormatIndentIncrease';
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
import Edit from '@mui/icons-material/Edit';
const LowPriority = 1;
function Divider() {
return <div className="divider" />;
}
function positionEditorElement(editor, rect) {
if (rect === null) {
editor.style.opacity = '0';
editor.style.top = '-1000px';
editor.style.left = '-1000px';
} else {
editor.style.opacity = '1';
editor.style.top = `${
rect.top + rect.height + window.pageYOffset + 10
}px`;
editor.style.left = `${
rect.left +
window.pageXOffset -
editor.offsetWidth / 25 +
rect.width / 25
}px`;
}
}
function getSelectedNode(selection) {
const anchor = selection.anchor;
const focus = selection.focus;
const anchorNode = selection.anchor.getNode();
const focusNode = selection.focus.getNode();
if (anchorNode === focusNode) {
return anchorNode;
}
const isBackward = selection.isBackward();
if (isBackward) {
return $isAtNodeEnd(focus) ? anchorNode : focusNode;
} else {
return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
}
}
function FloatingLinkEditor({ editor }) {
const editorRef = useRef(null);
const inputRef = useRef(null);
const mouseDownRef = useRef(false);
const [linkUrl, setLinkUrl] = useState('');
const [isEditMode, setEditMode] = useState(false);
const [lastSelection, setLastSelection] = useState(null);
const updateLinkEditor = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const node = getSelectedNode(selection);
const parent = node.getParent();
if ($isLinkNode(parent)) {
setLinkUrl(parent.getURL());
} else if ($isLinkNode(node)) {
setLinkUrl(node.getURL());
} else {
setLinkUrl('');
}
}
const editorElem = editorRef.current;
const nativeSelection = window.getSelection();
const activeElement = document.activeElement;
if (editorElem === null) {
return;
}
const rootElement = editor.getRootElement();
if (
selection !== null &&
!nativeSelection.isCollapsed &&
rootElement !== null &&
rootElement.contains(nativeSelection.anchorNode)
) {
const domRange = nativeSelection.getRangeAt(0);
let rect;
if (nativeSelection.anchorNode === rootElement) {
let inner = rootElement;
while (inner.firstElementChild != null) {
inner = inner.firstElementChild;
}
rect = inner.getBoundingClientRect();
} else {
rect = domRange.getBoundingClientRect();
}
if (!mouseDownRef.current) {
positionEditorElement(editorElem, rect);
}
setLastSelection(selection);
} else if (!activeElement || activeElement.className !== 'link-input') {
positionEditorElement(editorElem, null);
setLastSelection(null);
setEditMode(false);
setLinkUrl('');
}
return true;
}, [editor]);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateLinkEditor();
});
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
updateLinkEditor();
return true;
},
LowPriority,
),
);
}, [editor, updateLinkEditor]);
useEffect(() => {
editor.getEditorState().read(() => {
updateLinkEditor();
});
}, [editor, updateLinkEditor]);
useEffect(() => {
if (isEditMode && inputRef.current) {
inputRef.current.focus();
}
}, [isEditMode]);
return (
<div ref={editorRef} className="link-editor">
{isEditMode ? (
<input
ref={inputRef}
className="link-input"
value={linkUrl}
onChange={(event) => {
setLinkUrl(event.target.value);
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
if (lastSelection !== null) {
if (linkUrl !== '') {
editor.dispatchCommand(
TOGGLE_LINK_COMMAND,
linkUrl,
);
}
setEditMode(false);
}
} else if (event.key === 'Escape') {
event.preventDefault();
setEditMode(false);
}
}}
/>
) : (
<>
<div className="link-input">
<a
href={linkUrl}
target="_blank"
rel="noopener noreferrer"
>
{linkUrl}
</a>
<div
className="link-edit"
tabIndex={0}
onClick={() => {
setEditMode(true);
}}
onMouseDown={(event) => event.preventDefault()}
>
<Edit />
</div>
</div>
</>
)}
</div>
);
}
export default function ToolbarPlugin() {
const [editor] = useLexicalComposerContext();
const toolbarRef = useRef(null);
const [blockType, setBlockType] = useState('paragraph');
const [isLink, setIsLink] = useState(false);
const [isBold, setIsBold] = useState(false);
const [isItalic, setIsItalic] = useState(false);
const updateToolbar = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode();
const element =
anchorNode.getKey() === 'root'
? anchorNode
: anchorNode.getTopLevelElementOrThrow();
const elementKey = element.getKey();
const elementDOM = editor.getElementByKey(elementKey);
if (elementDOM !== null) {
if ($isListNode(element)) {
const parentList = $getNearestNodeOfType(
anchorNode,
ListNode,
);
const type = parentList
? parentList.getTag()
: element.getTag();
setBlockType(type);
} else {
const type = $isHeadingNode(element)
? element.getTag()
: element.getType();
setBlockType(type);
}
}
// Update text format
setIsBold(selection.hasFormat('bold'));
setIsItalic(selection.hasFormat('italic'));
// Update links
const node = getSelectedNode(selection);
const parent = node.getParent();
if ($isLinkNode(parent) || $isLinkNode(node)) {
setIsLink(true);
} else {
setIsLink(false);
}
}
}, [editor]);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateToolbar();
});
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_payload, newEditor) => {
updateToolbar();
return false;
},
LowPriority,
),
);
}, [editor, updateToolbar]);
const insertLink = useCallback(() => {
if (!isLink) {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://');
} else {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
}
}, [editor, isLink]);
const formatBulletList = () => {
if (blockType !== 'ul') {
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND);
} else {
editor.dispatchCommand(REMOVE_LIST_COMMAND);
}
};
const formatNumberedList = () => {
if (blockType !== 'ol') {
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND);
} else {
editor.dispatchCommand(REMOVE_LIST_COMMAND);
}
};
return (
<Grid container direction="row" ref={toolbarRef}>
<Grid item sm={12} md={12} lg={5} className="toolbar">
<span
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
}}
className={
'toolbar-item spaced ' + (isBold ? 'active' : '')
}
aria-label="Format Bold"
>
<FormatBoldIcon />
</span>
<span
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
}}
className={
'toolbar-item spaced ' + (isItalic ? 'active' : '')
}
aria-label="Format Italics"
>
<FormatItalicIcon />
</span>
<span
onClick={insertLink}
className={
'toolbar-item spaced ' + (isLink ? 'active' : '')
}
aria-label="Insert Link"
>
<LinkIcon />
</span>
{isLink &&
createPortal(
<FloatingLinkEditor editor={editor} />,
document.body,
)}
<Divider />
<span
onClick={formatBulletList}
className={'toolbar-item spaced '}
>
<FormatListBulletedIcon />
</span>
<span
onClick={formatNumberedList}
className={'toolbar-item spaced '}
>
<FormatListNumberedIcon />
</span>
</Grid>
<Grid item sm={12} md={12} lg={7} className="toolbar">
<span
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left');
}}
className="toolbar-item spaced"
aria-label="Left Align"
>
<FormatAlignLeftIcon />
</span>
<span
onClick={() => {
editor.dispatchCommand(
FORMAT_ELEMENT_COMMAND,
'center',
);
}}
className="toolbar-item spaced"
aria-label="Center Align"
>
<FormatAlignCenterIcon />
</span>
<span
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right');
}}
className="toolbar-item spaced"
aria-label="Right Align"
>
<FormatAlignRightIcon />
</span>
<span
onClick={() => {
editor.dispatchCommand(
FORMAT_ELEMENT_COMMAND,
'justify',
);
}}
className="toolbar-item spaced"
aria-label="Justify Align"
>
<FormatAlignJustifyIcon />
</span>
<Divider />
<span
onClick={() => {
editor.dispatchCommand(INDENT_CONTENT_COMMAND);
}}
className={'toolbar-item spaced '}
aria-label="Format Indent"
>
<FormatIndentIncreaseIcon />
</span>
<span
onClick={() => {
editor.dispatchCommand(OUTDENT_CONTENT_COMMAND);
}}
className={'toolbar-item spaced '}
aria-label="Format Outdent"
>
<FormatIndentDecreaseIcon />
</span>
</Grid>
</Grid>
);
}
Issue Analytics
- State:
- Created a year ago
- Comments:5 (3 by maintainers)
Top GitHub Comments
Hi @yuuminakamura! I’m unable to repro this on the current Lexical Playground, could you attach a screen recording of you reproducing this bug on our playground?
@tylerjbainbridge can you take a look?
Also, it looks like you’re on an older version of Lexical - it might be a good idea to try upgrading to the latest and ensuring that this still repros.