Skip to content

Commit 94cfcd0

Browse files
author
ilimei
committed
✨ feat: Add support for link text editing in the editor
1 parent 225a6f2 commit 94cfcd0

5 files changed

Lines changed: 131 additions & 31 deletions

File tree

src/locale/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export default {
88
},
99
link: {
1010
edit: 'Edit Link',
11+
editLinkTitle: 'Link',
12+
editTextTitle: 'Text',
1113
open: 'Open Link',
1214
placeholder: 'Enter link URL',
1315
unlink: 'Unlink Link',

src/plugins/link/command/index.ts

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import { LinkNode } from '@lexical/link';
2+
import { mergeRegister } from '@lexical/utils';
13
import {
24
$createTextNode,
5+
$getNodeByKey,
36
$insertNodes,
47
COMMAND_PRIORITY_EDITOR,
58
LexicalEditor,
9+
NodeKey,
610
createCommand,
711
} from 'lexical';
812

@@ -12,21 +16,47 @@ export const INSERT_LINK_COMMAND = createCommand<{ title?: string; url?: string
1216
'INSERT_LINK_COMMAND',
1317
);
1418

19+
export const UPDATE_LINK_TEXT_COMMAND = createCommand<{ key: NodeKey; text: string }>(
20+
'UPDATE_LINK_TEXT_COMMAND',
21+
);
22+
1523
export function registerLinkCommand(editor: LexicalEditor) {
16-
return editor.registerCommand(
17-
INSERT_LINK_COMMAND,
18-
(payload) => {
19-
const { url, title = url } = payload;
20-
21-
editor.update(() => {
22-
const linkNode = $createLinkNode(url, { title });
23-
const textNode = $createTextNode(title);
24-
linkNode.append(textNode);
25-
$insertNodes([linkNode]);
26-
});
27-
28-
return false;
29-
},
30-
COMMAND_PRIORITY_EDITOR, // Priority
24+
return mergeRegister(
25+
editor.registerCommand(
26+
INSERT_LINK_COMMAND,
27+
(payload) => {
28+
const { url, title = url } = payload;
29+
30+
editor.update(() => {
31+
const linkNode = $createLinkNode(url, { title });
32+
const textNode = $createTextNode(title);
33+
linkNode.append(textNode);
34+
$insertNodes([linkNode]);
35+
});
36+
37+
return false;
38+
},
39+
COMMAND_PRIORITY_EDITOR, // Priority
40+
),
41+
editor.registerCommand(
42+
UPDATE_LINK_TEXT_COMMAND,
43+
(payload) => {
44+
const { key, text } = payload;
45+
46+
editor.update(() => {
47+
const linkNode = $getNodeByKey<LinkNode>(key);
48+
if (linkNode) {
49+
const newLinkNode = $createLinkNode(linkNode.getURL(), { title: text });
50+
const textNode = $createTextNode(text);
51+
newLinkNode.append(textNode);
52+
linkNode?.replace(newLinkNode);
53+
newLinkNode.select(1);
54+
}
55+
});
56+
57+
return false;
58+
},
59+
COMMAND_PRIORITY_EDITOR,
60+
),
3161
);
3262
}

src/plugins/link/react/components/LinkEdit.tsx

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import {
2121
} from 'react';
2222

2323
import { useLexicalComposerContext, useLexicalEditor } from '@/editor-kernel/react';
24+
import { useTranslation } from '@/editor-kernel/react/useTranslation';
2425

26+
import { UPDATE_LINK_TEXT_COMMAND } from '../../command';
2527
import { LinkNode } from '../../node/LinkNode';
2628
import { useStyles } from '../style';
2729

@@ -34,9 +36,12 @@ export const LinkEdit: FC = () => {
3436
const divRef = useRef<HTMLDivElement>(null);
3537
const linkNodeRef = useRef<LinkNode | null>(null);
3638
const linkInputRef = useRef<InputRef | null>(null);
39+
const linkTextInputRef = useRef<InputRef | null>(null);
3740
const [linkUrl, setLinkUrl] = useState('');
41+
const [linkText, setLinkText] = useState('');
3842
const [linkDom, setLinkDom] = useState<HTMLElement | null>(null);
3943
const [editor] = useLexicalComposerContext();
44+
const t = useTranslation();
4045
const { styles, theme } = useStyles();
4146

4247
useEffect(() => {
@@ -57,29 +62,64 @@ export const LinkEdit: FC = () => {
5762
const handleKeyDown = useCallback(
5863
(event: KeyboardEvent<HTMLInputElement>) => {
5964
const lexicalEditor = editor.getLexicalEditor();
60-
if (!linkNodeRef.current || !linkInputRef.current || !lexicalEditor) {
65+
if (
66+
!linkNodeRef.current ||
67+
!linkInputRef.current ||
68+
!linkTextInputRef.current ||
69+
!lexicalEditor
70+
) {
6171
return;
6272
}
6373

6474
const linkNode = linkNodeRef.current;
6575
const input = linkInputRef.current;
6676
const inputDOM = input.input as HTMLInputElement;
67-
if (event.key === 'Enter') {
68-
event.preventDefault();
69-
const currentURL = lexicalEditor.read(() => linkNode.getURL());
70-
if (currentURL !== inputDOM.value) {
71-
lexicalEditor.update(() => {
72-
linkNode.setURL(inputDOM.value);
77+
const textInput = linkTextInputRef.current;
78+
const textInputDOM = textInput.input as HTMLInputElement;
79+
switch (event.key) {
80+
case 'Enter': {
81+
event.preventDefault();
82+
if (event.currentTarget === inputDOM) {
83+
const currentURL = lexicalEditor.read(() => linkNode.getURL());
84+
if (currentURL !== inputDOM.value) {
85+
lexicalEditor.update(() => {
86+
linkNode.setURL(inputDOM.value);
87+
// lexicalEditor.focus();
88+
textInputDOM.focus();
89+
});
90+
} else {
91+
// lexicalEditor.focus();
92+
textInputDOM.focus();
93+
}
94+
} else if (event.currentTarget === textInputDOM) {
95+
const currentText = lexicalEditor.read(() => linkNode.getTextContent());
96+
if (currentText !== textInputDOM.value) {
97+
lexicalEditor.dispatchCommand(UPDATE_LINK_TEXT_COMMAND, {
98+
key: linkNode.getKey(),
99+
text: textInputDOM.value,
100+
});
101+
lexicalEditor.focus();
102+
} else {
103+
lexicalEditor.focus();
104+
}
105+
}
106+
return;
107+
}
108+
case 'Tab': {
109+
event.preventDefault();
110+
if (event.currentTarget === inputDOM) {
111+
textInputDOM.focus();
112+
} else {
73113
lexicalEditor.focus();
74-
});
75-
} else {
114+
}
115+
return;
116+
}
117+
case 'Escape': {
76118
lexicalEditor.focus();
119+
120+
break;
77121
}
78-
return;
79-
} else if (event.key === 'Escape' || event.key === 'Tab') {
80-
event.preventDefault();
81-
lexicalEditor.focus();
82-
return;
122+
// No default
83123
}
84124
},
85125
[linkNodeRef, linkInputRef],
@@ -93,6 +133,7 @@ export const LinkEdit: FC = () => {
93133
if (!payload.linkNode || !payload.linkNodeDOM) {
94134
setLinkDom(null);
95135
setLinkUrl('');
136+
setLinkText('');
96137
if (divRef.current) {
97138
divRef.current.style.left = '-9999px';
98139
divRef.current.style.top = '-9999px';
@@ -101,6 +142,7 @@ export const LinkEdit: FC = () => {
101142
}
102143
linkNodeRef.current = payload.linkNode;
103144
setLinkUrl(payload.linkNode.getURL());
145+
setLinkText(payload.linkNode.getTextContent());
104146
setLinkDom(payload.linkNodeDOM);
105147
return true;
106148
},
@@ -115,6 +157,7 @@ export const LinkEdit: FC = () => {
115157
}
116158
linkNodeRef.current = null;
117159
setLinkUrl('');
160+
setLinkText('');
118161
setLinkDom(null);
119162
return true;
120163
},
@@ -138,6 +181,7 @@ export const LinkEdit: FC = () => {
138181

139182
return (
140183
<div className={styles.editor_linkEdit} ref={divRef}>
184+
<div>{t('link.editLinkTitle')}</div>
141185
<Input
142186
onChange={(e: ChangeEvent<HTMLInputElement>) => {
143187
// Handle link URL change
@@ -152,6 +196,16 @@ export const LinkEdit: FC = () => {
152196
value={linkUrl}
153197
variant={'outlined'}
154198
/>
199+
<div>{t('link.editTextTitle')}</div>
200+
<Input
201+
onChange={(e: ChangeEvent<HTMLInputElement>) => {
202+
// Handle link text change
203+
setLinkText(e.target.value);
204+
}}
205+
onKeyDown={handleKeyDown}
206+
ref={linkTextInputRef}
207+
value={linkText}
208+
/>
155209
</div>
156210
);
157211
};

src/plugins/link/react/style.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createStyles } from 'antd-style';
22

3-
export const useStyles = createStyles(({ css }) => {
3+
export const useStyles = createStyles(({ css, token }) => {
44
const position = css`
55
position: absolute;
66
z-index: 999;
@@ -9,7 +9,19 @@ export const useStyles = createStyles(({ css }) => {
99
`;
1010

1111
return {
12-
editor_linkEdit: position,
12+
editor_linkEdit: css`
13+
position: absolute;
14+
z-index: 999;
15+
inset-block-start: -9999px;
16+
inset-inline-start: -9999px;
17+
18+
padding: 10px;
19+
border: ${token.colorInfoBorder};
20+
border-radius: ${token.borderRadiusLG}px;
21+
22+
background: ${token.colorBgContainer};
23+
box-shadow: ${token.boxShadow};
24+
`,
1325

1426
editor_linkPlugin: position,
1527

src/react/EditorProvider/demos/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const customLocale = {
1313
},
1414
link: {
1515
edit: '编辑链接',
16+
editLinkTitle: '链接',
17+
editTextTitle: '文本',
1618
open: '打开链接',
1719
placeholder: '输入链接 URL',
1820
unlink: '取消链接',

0 commit comments

Comments
 (0)