Lexical: Added auto links on enter/space
This commit is contained in:
		
							parent
							
								
									a8ef820443
								
							
						
					
					
						commit
						97b201f61f
					
				| 
						 | 
				
			
			@ -15,6 +15,7 @@ import {el} from "./utils/dom";
 | 
			
		|||
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 = {
 | 
			
		||||
| 
						 | 
				
			
			@ -64,6 +65,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
 | 
			
		|||
        registerTaskListHandler(editor, editArea),
 | 
			
		||||
        registerDropPasteHandling(context),
 | 
			
		||||
        registerNodeResizer(context),
 | 
			
		||||
        registerAutoLinks(editor),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    listenToCommonEvents(editor);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,91 @@
 | 
			
		|||
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');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,74 @@
 | 
			
		|||
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();
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -51,6 +51,10 @@ export function $getNodeFromSelection(selection: BaseSelection | null, matcher:
 | 
			
		|||
    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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue