mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-07-28 17:02:04 +03:00
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.
This commit is contained in:
@ -19,6 +19,7 @@ import {contextToolbars, getBasicEditorToolbar, getMainEditorFullToolbar} from "
|
|||||||
import {modals} from "./ui/defaults/modals";
|
import {modals} from "./ui/defaults/modals";
|
||||||
import {CodeBlockDecorator} from "./ui/decorators/code-block";
|
import {CodeBlockDecorator} from "./ui/decorators/code-block";
|
||||||
import {DiagramDecorator} from "./ui/decorators/diagram";
|
import {DiagramDecorator} from "./ui/decorators/diagram";
|
||||||
|
import {registerMouseHandling} from "./services/mouse-handling";
|
||||||
|
|
||||||
const theme = {
|
const theme = {
|
||||||
text: {
|
text: {
|
||||||
@ -51,6 +52,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
|
|||||||
registerHistory(editor, createEmptyHistoryState(), 300),
|
registerHistory(editor, createEmptyHistoryState(), 300),
|
||||||
registerShortcuts(context),
|
registerShortcuts(context),
|
||||||
registerKeyboardHandling(context),
|
registerKeyboardHandling(context),
|
||||||
|
registerMouseHandling(context),
|
||||||
registerTableResizer(editor, context.scrollDOM),
|
registerTableResizer(editor, context.scrollDOM),
|
||||||
registerTableSelectionHandler(editor),
|
registerTableSelectionHandler(editor),
|
||||||
registerTaskListHandler(editor, context.editorDOM),
|
registerTaskListHandler(editor, context.editorDOM),
|
||||||
|
@ -849,3 +849,19 @@ export function dispatchKeydownEventForSelectedNode(editor: LexicalEditor, 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();
|
||||||
|
}
|
@ -48,14 +48,6 @@ export type SerializedLinkNode = Spread<
|
|||||||
|
|
||||||
type LinkHTMLElementType = HTMLAnchorElement | HTMLSpanElement;
|
type LinkHTMLElementType = HTMLAnchorElement | HTMLSpanElement;
|
||||||
|
|
||||||
const SUPPORTED_URL_PROTOCOLS = new Set([
|
|
||||||
'http:',
|
|
||||||
'https:',
|
|
||||||
'mailto:',
|
|
||||||
'sms:',
|
|
||||||
'tel:',
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** @noInheritDoc */
|
/** @noInheritDoc */
|
||||||
export class LinkNode extends ElementNode {
|
export class LinkNode extends ElementNode {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
@ -90,7 +82,7 @@ export class LinkNode extends ElementNode {
|
|||||||
|
|
||||||
createDOM(config: EditorConfig): LinkHTMLElementType {
|
createDOM(config: EditorConfig): LinkHTMLElementType {
|
||||||
const element = document.createElement('a');
|
const element = document.createElement('a');
|
||||||
element.href = this.sanitizeUrl(this.__url);
|
element.href = this.__url;
|
||||||
if (this.__target !== null) {
|
if (this.__target !== null) {
|
||||||
element.target = this.__target;
|
element.target = this.__target;
|
||||||
}
|
}
|
||||||
@ -166,19 +158,6 @@ export class LinkNode extends ElementNode {
|
|||||||
return node;
|
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 {
|
exportJSON(): SerializedLinkNode | SerializedAutoLinkNode {
|
||||||
return {
|
return {
|
||||||
...super.exportJSON(),
|
...super.exportJSON(),
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
63
resources/js/wysiwyg/services/mouse-handling.ts
Normal file
63
resources/js/wysiwyg/services/mouse-handling.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
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();
|
||||||
|
};
|
||||||
|
}
|
Reference in New Issue
Block a user