mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-01-19 08:42:48 +08:00
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);
|
||||
|
|
91
resources/js/wysiwyg/services/__tests__/auto-links.test.ts
Normal file
91
resources/js/wysiwyg/services/__tests__/auto-links.test.ts
Normal file
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
74
resources/js/wysiwyg/services/auto-links.ts
Normal file
74
resources/js/wysiwyg/services/auto-links.ts
Normal file
|
@ -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
Block a user