- Removed URL protocol allow-list to allow any as per old editor.
- Added mouse handling, so that clicks below many last hard-to-escape
block types will add an empty new paragraph for easy escaping &
editing.
import {modals} from "./ui/defaults/modals";
import {CodeBlockDecorator} from "./ui/decorators/code-block";
import {DiagramDecorator} from "./ui/decorators/diagram";
+import {registerMouseHandling} from "./services/mouse-handling";
const theme = {
text: {
registerHistory(editor, createEmptyHistoryState(), 300),
registerShortcuts(context),
registerKeyboardHandling(context),
+ registerMouseHandling(context),
registerTableResizer(editor, context.scrollDOM),
registerTableSelectionHandler(editor),
registerTaskListHandler(editor, context.editorDOM),
dispatchKeydownEventForNode(node, editor, key);
}
});
+}
+
+export function dispatchEditorMouseClick(editor: LexicalEditor, clientX: number, clientY: number) {
+ const dom = editor.getRootElement();
+ if (!dom) {
+ return;
+ }
+
+ const event = new MouseEvent('click', {
+ clientX: clientX,
+ clientY: clientY,
+ bubbles: true,
+ cancelable: true,
+ });
+ dom?.dispatchEvent(event);
+ editor.commitUpdates();
}
\ No newline at end of file
type LinkHTMLElementType = HTMLAnchorElement | HTMLSpanElement;
-const SUPPORTED_URL_PROTOCOLS = new Set([
- 'http:',
- 'https:',
- 'mailto:',
- 'sms:',
- 'tel:',
-]);
-
/** @noInheritDoc */
export class LinkNode extends ElementNode {
/** @internal */
createDOM(config: EditorConfig): LinkHTMLElementType {
const element = document.createElement('a');
- element.href = this.sanitizeUrl(this.__url);
+ element.href = this.__url;
if (this.__target !== null) {
element.target = this.__target;
}
return node;
}
- sanitizeUrl(url: string): string {
- try {
- const parsedUrl = new URL(url);
- // eslint-disable-next-line no-script-url
- if (!SUPPORTED_URL_PROTOCOLS.has(parsedUrl.protocol)) {
- return 'about:blank';
- }
- } catch {
- return url;
- }
- return url;
- }
-
exportJSON(): SerializedLinkNode | SerializedAutoLinkNode {
return {
...super.exportJSON(),
--- /dev/null
+import {
+ createTestContext, destroyFromContext, dispatchEditorMouseClick,
+} from "lexical/__tests__/utils";
+import {
+ $getRoot, LexicalEditor, LexicalNode,
+ ParagraphNode,
+} from "lexical";
+import {registerRichText} from "@lexical/rich-text";
+import {EditorUiContext} from "../../ui/framework/core";
+import {registerMouseHandling} from "../mouse-handling";
+import {$createTableNode, TableNode} from "@lexical/table";
+
+describe('Mouse-handling service tests', () => {
+
+ let context!: EditorUiContext;
+ let editor!: LexicalEditor;
+
+ beforeEach(() => {
+ context = createTestContext();
+ editor = context.editor;
+ registerRichText(editor);
+ registerMouseHandling(context);
+ });
+
+ afterEach(() => {
+ destroyFromContext(context);
+ });
+
+ test('Click below last table inserts new empty paragraph', () => {
+ let tableNode!: TableNode;
+ let lastRootChild!: LexicalNode|null;
+
+ editor.updateAndCommit(() => {
+ tableNode = $createTableNode();
+ $getRoot().append(tableNode);
+ lastRootChild = $getRoot().getLastChild();
+ });
+
+ expect(lastRootChild).toBeInstanceOf(TableNode);
+
+ const tableDOM = editor.getElementByKey(tableNode.getKey());
+ const rect = tableDOM?.getBoundingClientRect();
+ dispatchEditorMouseClick(editor, 0, (rect?.bottom || 0) + 1)
+
+ editor.getEditorState().read(() => {
+ lastRootChild = $getRoot().getLastChild();
+ });
+
+ expect(lastRootChild).toBeInstanceOf(ParagraphNode);
+ });
+});
\ No newline at end of file
--- /dev/null
+import {EditorUiContext} from "../ui/framework/core";
+import {
+ $createParagraphNode, $getRoot,
+ $getSelection,
+ $isDecoratorNode, CLICK_COMMAND,
+ COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND,
+ KEY_BACKSPACE_COMMAND,
+ KEY_DELETE_COMMAND,
+ KEY_ENTER_COMMAND, KEY_TAB_COMMAND,
+ LexicalEditor,
+ LexicalNode
+} from "lexical";
+import {$isImageNode} from "@lexical/rich-text/LexicalImageNode";
+import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode";
+import {getLastSelection} from "../utils/selection";
+import {$getNearestNodeBlockParent, $getParentOfType, $selectOrCreateAdjacent} from "../utils/nodes";
+import {$setInsetForSelection} from "../utils/lists";
+import {$isListItemNode} from "@lexical/list";
+import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
+import {$isDiagramNode} from "../utils/diagrams";
+import {$isTableNode} from "@lexical/table";
+
+function isHardToEscapeNode(node: LexicalNode): boolean {
+ return $isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node) || $isDiagramNode(node) || $isTableNode(node);
+}
+
+function insertBelowLastNode(context: EditorUiContext, event: MouseEvent): boolean {
+ const lastNode = $getRoot().getLastChild();
+ if (!lastNode || !isHardToEscapeNode(lastNode)) {
+ return false;
+ }
+
+ const lastNodeDom = context.editor.getElementByKey(lastNode.getKey());
+ if (!lastNodeDom) {
+ return false;
+ }
+
+ const nodeBounds = lastNodeDom.getBoundingClientRect();
+ const isClickBelow = event.clientY > nodeBounds.bottom;
+ if (isClickBelow) {
+ context.editor.update(() => {
+ const newNode = $createParagraphNode();
+ $getRoot().append(newNode);
+ newNode.select();
+ });
+ return true;
+ }
+
+ return false;
+}
+
+
+export function registerMouseHandling(context: EditorUiContext): () => void {
+ const unregisterClick = context.editor.registerCommand(CLICK_COMMAND, (event): boolean => {
+ insertBelowLastNode(context, event);
+ return false;
+ }, COMMAND_PRIORITY_LOW);
+
+
+ return () => {
+ unregisterClick();
+ };
+}
\ No newline at end of file