import {registerShortcuts} from "./services/shortcuts";
import {registerNodeResizer} from "./ui/framework/helpers/node-resizer";
import {registerKeyboardHandling} from "./services/keyboard-handling";
+import {registerAutoLinks} from "./services/auto-links";
export function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
const config: CreateEditorArgs = {
registerTaskListHandler(editor, editArea),
registerDropPasteHandling(context),
registerNodeResizer(context),
+ registerAutoLinks(editor),
);
listenToCommonEvents(editor);
--- /dev/null
+import {initializeUnitTest} from "lexical/__tests__/utils";
+import {SerializedLinkNode} from "@lexical/link";
+import {
+ $getRoot,
+ ParagraphNode,
+ SerializedParagraphNode,
+ SerializedTextNode,
+ TextNode
+} from "lexical";
+import {registerAutoLinks} from "../auto-links";
+
+describe('Auto-link service tests', () => {
+ initializeUnitTest((testEnv) => {
+
+ test('space after link in text', async () => {
+ const {editor} = testEnv;
+
+ registerAutoLinks(editor);
+ let pNode!: ParagraphNode;
+
+ editor.update(() => {
+ pNode = new ParagraphNode();
+ const text = new TextNode('Some https://example.com?test=true text');
+ pNode.append(text);
+ $getRoot().append(pNode);
+
+ text.select(35, 35);
+ });
+
+ editor.commitUpdates();
+
+ const pDomEl = editor.getElementByKey(pNode.getKey());
+ const event = new KeyboardEvent('keydown', {
+ bubbles: true,
+ cancelable: true,
+ key: ' ',
+ keyCode: 62,
+ });
+ pDomEl?.dispatchEvent(event);
+
+ editor.commitUpdates();
+
+ const paragraph = editor!.getEditorState().toJSON().root
+ .children[0] as SerializedParagraphNode;
+ expect(paragraph.children[1].type).toBe('link');
+
+ const link = paragraph.children[1] as SerializedLinkNode;
+ expect(link.url).toBe('https://example.com?test=true');
+ const linkText = link.children[0] as SerializedTextNode;
+ expect(linkText.text).toBe('https://example.com?test=true');
+ });
+
+ test('enter after link in text', async () => {
+ const {editor} = testEnv;
+
+ registerAutoLinks(editor);
+ let pNode!: ParagraphNode;
+
+ editor.update(() => {
+ pNode = new ParagraphNode();
+ const text = new TextNode('Some https://example.com?test=true text');
+ pNode.append(text);
+ $getRoot().append(pNode);
+
+ text.select(35, 35);
+ });
+
+ editor.commitUpdates();
+
+ const pDomEl = editor.getElementByKey(pNode.getKey());
+ const event = new KeyboardEvent('keydown', {
+ bubbles: true,
+ cancelable: true,
+ key: 'Enter',
+ keyCode: 66,
+ });
+ pDomEl?.dispatchEvent(event);
+
+ editor.commitUpdates();
+
+ const paragraph = editor!.getEditorState().toJSON().root
+ .children[0] as SerializedParagraphNode;
+ expect(paragraph.children[1].type).toBe('link');
+
+ const link = paragraph.children[1] as SerializedLinkNode;
+ expect(link.url).toBe('https://example.com?test=true');
+ const linkText = link.children[0] as SerializedTextNode;
+ expect(linkText.text).toBe('https://example.com?test=true');
+ });
+ });
+});
\ No newline at end of file
--- /dev/null
+import {
+ $getSelection, BaseSelection,
+ COMMAND_PRIORITY_NORMAL,
+ KEY_ENTER_COMMAND,
+ KEY_SPACE_COMMAND,
+ LexicalEditor,
+ TextNode
+} from "lexical";
+import {$getTextNodeFromSelection} from "../utils/selection";
+import {$createLinkNode, LinkNode} from "@lexical/link";
+
+
+function isLinkText(text: string): boolean {
+ const lower = text.toLowerCase();
+ if (!lower.startsWith('http')) {
+ return false;
+ }
+
+ const linkRegex = /(http|https):\/\/(\S+)\.\S+$/;
+ return linkRegex.test(text);
+}
+
+
+function handlePotentialLinkEvent(node: TextNode, selection: BaseSelection, editor: LexicalEditor) {
+ const selectionRange = selection.getStartEndPoints();
+ if (!selectionRange) {
+ return;
+ }
+
+ const cursorPoint = selectionRange[0].offset - 1;
+ const nodeText = node.getTextContent();
+ const rTrimText = nodeText.slice(0, cursorPoint);
+ const priorSpaceIndex = rTrimText.lastIndexOf(' ');
+ const startIndex = priorSpaceIndex + 1;
+ const textSegment = nodeText.slice(startIndex, cursorPoint);
+
+ if (!isLinkText(textSegment)) {
+ return;
+ }
+
+ editor.update(() => {
+ const linkNode: LinkNode = $createLinkNode(textSegment);
+ linkNode.append(new TextNode(textSegment));
+
+ const splits = node.splitText(startIndex, cursorPoint);
+ const targetIndex = splits.length === 3 ? 1 : 0;
+ const targetText = splits[targetIndex];
+ if (targetText) {
+ targetText.replace(linkNode);
+ }
+ });
+}
+
+
+export function registerAutoLinks(editor: LexicalEditor): () => void {
+
+ const handler = (payload: KeyboardEvent): boolean => {
+ const selection = $getSelection();
+ const textNode = $getTextNodeFromSelection(selection);
+ if (textNode && selection) {
+ handlePotentialLinkEvent(textNode, selection, editor);
+ }
+
+ return false;
+ };
+
+ const unregisterSpace = editor.registerCommand(KEY_SPACE_COMMAND, handler, COMMAND_PRIORITY_NORMAL);
+ const unregisterEnter = editor.registerCommand(KEY_ENTER_COMMAND, handler, COMMAND_PRIORITY_NORMAL);
+
+ return (): void => {
+ unregisterSpace();
+ unregisterEnter();
+ };
+}
\ No newline at end of file
return null;
}
+export function $getTextNodeFromSelection(selection: BaseSelection | null): TextNode|null {
+ return $getNodeFromSelection(selection, $isTextNode) as TextNode|null;
+}
+
export function $selectionContainsTextFormat(selection: BaseSelection | null, format: TextFormatType): boolean {
if (!selection) {
return false;