Skip to content

Commit c54101c

Browse files
committed
Lexical: Updated URL handling, added mouse handling
- 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.
1 parent 865e5ae commit c54101c

File tree

5 files changed

+133
-22
lines changed

5 files changed

+133
-22
lines changed

resources/js/wysiwyg/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {contextToolbars, getBasicEditorToolbar, getMainEditorFullToolbar} from "
1919
import {modals} from "./ui/defaults/modals";
2020
import {CodeBlockDecorator} from "./ui/decorators/code-block";
2121
import {DiagramDecorator} from "./ui/decorators/diagram";
22+
import {registerMouseHandling} from "./services/mouse-handling";
2223

2324
const theme = {
2425
text: {
@@ -51,6 +52,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
5152
registerHistory(editor, createEmptyHistoryState(), 300),
5253
registerShortcuts(context),
5354
registerKeyboardHandling(context),
55+
registerMouseHandling(context),
5456
registerTableResizer(editor, context.scrollDOM),
5557
registerTableSelectionHandler(editor),
5658
registerTaskListHandler(editor, context.editorDOM),

resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,4 +848,20 @@ export function dispatchKeydownEventForSelectedNode(editor: LexicalEditor, key:
848848
dispatchKeydownEventForNode(node, editor, key);
849849
}
850850
});
851+
}
852+
853+
export function dispatchEditorMouseClick(editor: LexicalEditor, clientX: number, clientY: number) {
854+
const dom = editor.getRootElement();
855+
if (!dom) {
856+
return;
857+
}
858+
859+
const event = new MouseEvent('click', {
860+
clientX: clientX,
861+
clientY: clientY,
862+
bubbles: true,
863+
cancelable: true,
864+
});
865+
dom?.dispatchEvent(event);
866+
editor.commitUpdates();
851867
}

resources/js/wysiwyg/lexical/link/index.ts

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,6 @@ export type SerializedLinkNode = Spread<
4848

4949
type LinkHTMLElementType = HTMLAnchorElement | HTMLSpanElement;
5050

51-
const SUPPORTED_URL_PROTOCOLS = new Set([
52-
'http:',
53-
'https:',
54-
'mailto:',
55-
'sms:',
56-
'tel:',
57-
]);
58-
5951
/** @noInheritDoc */
6052
export class LinkNode extends ElementNode {
6153
/** @internal */
@@ -90,7 +82,7 @@ export class LinkNode extends ElementNode {
9082

9183
createDOM(config: EditorConfig): LinkHTMLElementType {
9284
const element = document.createElement('a');
93-
element.href = this.sanitizeUrl(this.__url);
85+
element.href = this.__url;
9486
if (this.__target !== null) {
9587
element.target = this.__target;
9688
}
@@ -166,19 +158,6 @@ export class LinkNode extends ElementNode {
166158
return node;
167159
}
168160

169-
sanitizeUrl(url: string): string {
170-
try {
171-
const parsedUrl = new URL(url);
172-
// eslint-disable-next-line no-script-url
173-
if (!SUPPORTED_URL_PROTOCOLS.has(parsedUrl.protocol)) {
174-
return 'about:blank';
175-
}
176-
} catch {
177-
return url;
178-
}
179-
return url;
180-
}
181-
182161
exportJSON(): SerializedLinkNode | SerializedAutoLinkNode {
183162
return {
184163
...super.exportJSON(),
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {
2+
createTestContext, destroyFromContext, dispatchEditorMouseClick,
3+
} from "lexical/__tests__/utils";
4+
import {
5+
$getRoot, LexicalEditor, LexicalNode,
6+
ParagraphNode,
7+
} from "lexical";
8+
import {registerRichText} from "@lexical/rich-text";
9+
import {EditorUiContext} from "../../ui/framework/core";
10+
import {registerMouseHandling} from "../mouse-handling";
11+
import {$createTableNode, TableNode} from "@lexical/table";
12+
13+
describe('Mouse-handling service tests', () => {
14+
15+
let context!: EditorUiContext;
16+
let editor!: LexicalEditor;
17+
18+
beforeEach(() => {
19+
context = createTestContext();
20+
editor = context.editor;
21+
registerRichText(editor);
22+
registerMouseHandling(context);
23+
});
24+
25+
afterEach(() => {
26+
destroyFromContext(context);
27+
});
28+
29+
test('Click below last table inserts new empty paragraph', () => {
30+
let tableNode!: TableNode;
31+
let lastRootChild!: LexicalNode|null;
32+
33+
editor.updateAndCommit(() => {
34+
tableNode = $createTableNode();
35+
$getRoot().append(tableNode);
36+
lastRootChild = $getRoot().getLastChild();
37+
});
38+
39+
expect(lastRootChild).toBeInstanceOf(TableNode);
40+
41+
const tableDOM = editor.getElementByKey(tableNode.getKey());
42+
const rect = tableDOM?.getBoundingClientRect();
43+
dispatchEditorMouseClick(editor, 0, (rect?.bottom || 0) + 1)
44+
45+
editor.getEditorState().read(() => {
46+
lastRootChild = $getRoot().getLastChild();
47+
});
48+
49+
expect(lastRootChild).toBeInstanceOf(ParagraphNode);
50+
});
51+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {EditorUiContext} from "../ui/framework/core";
2+
import {
3+
$createParagraphNode, $getRoot,
4+
$getSelection,
5+
$isDecoratorNode, CLICK_COMMAND,
6+
COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND,
7+
KEY_BACKSPACE_COMMAND,
8+
KEY_DELETE_COMMAND,
9+
KEY_ENTER_COMMAND, KEY_TAB_COMMAND,
10+
LexicalEditor,
11+
LexicalNode
12+
} from "lexical";
13+
import {$isImageNode} from "@lexical/rich-text/LexicalImageNode";
14+
import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode";
15+
import {getLastSelection} from "../utils/selection";
16+
import {$getNearestNodeBlockParent, $getParentOfType, $selectOrCreateAdjacent} from "../utils/nodes";
17+
import {$setInsetForSelection} from "../utils/lists";
18+
import {$isListItemNode} from "@lexical/list";
19+
import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
20+
import {$isDiagramNode} from "../utils/diagrams";
21+
import {$isTableNode} from "@lexical/table";
22+
23+
function isHardToEscapeNode(node: LexicalNode): boolean {
24+
return $isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node) || $isDiagramNode(node) || $isTableNode(node);
25+
}
26+
27+
function insertBelowLastNode(context: EditorUiContext, event: MouseEvent): boolean {
28+
const lastNode = $getRoot().getLastChild();
29+
if (!lastNode || !isHardToEscapeNode(lastNode)) {
30+
return false;
31+
}
32+
33+
const lastNodeDom = context.editor.getElementByKey(lastNode.getKey());
34+
if (!lastNodeDom) {
35+
return false;
36+
}
37+
38+
const nodeBounds = lastNodeDom.getBoundingClientRect();
39+
const isClickBelow = event.clientY > nodeBounds.bottom;
40+
if (isClickBelow) {
41+
context.editor.update(() => {
42+
const newNode = $createParagraphNode();
43+
$getRoot().append(newNode);
44+
newNode.select();
45+
});
46+
return true;
47+
}
48+
49+
return false;
50+
}
51+
52+
53+
export function registerMouseHandling(context: EditorUiContext): () => void {
54+
const unregisterClick = context.editor.registerCommand(CLICK_COMMAND, (event): boolean => {
55+
insertBelowLastNode(context, event);
56+
return false;
57+
}, COMMAND_PRIORITY_LOW);
58+
59+
60+
return () => {
61+
unregisterClick();
62+
};
63+
}

0 commit comments

Comments
 (0)