Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lang/en/editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
'superscript' => 'Superscript',
'subscript' => 'Subscript',
'text_color' => 'Text color',
'highlight_color' => 'Highlight color',
'custom_color' => 'Custom color',
'remove_color' => 'Remove color',
'background_color' => 'Background color',
Expand Down
2 changes: 2 additions & 0 deletions resources/js/wysiwyg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {contextToolbars, getBasicEditorToolbar, getMainEditorFullToolbar} from "
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: {
Expand Down Expand Up @@ -51,6 +52,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
registerHistory(editor, createEmptyHistoryState(), 300),
registerShortcuts(context),
registerKeyboardHandling(context),
registerMouseHandling(context),
registerTableResizer(editor, context.scrollDOM),
registerTableSelectionHandler(editor),
registerTaskListHandler(editor, context.editorDOM),
Expand Down
16 changes: 16 additions & 0 deletions resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -848,4 +848,20 @@ export function dispatchKeydownEventForSelectedNode(editor: LexicalEditor, key:
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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ describe('HTML', () => {
});

expect(html).toBe(
'<p>Hello</p><p>World</p>',
'<p>Hello</p>\n<p>World</p>',
);
});

Expand Down
13 changes: 12 additions & 1 deletion resources/js/wysiwyg/lexical/html/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,18 @@ export function $generateHtmlFromNodes(
$appendNodesToHTML(editor, topLevelNode, container, selection);
}

return container.innerHTML;
const nodeCode = [];
for (const node of container.childNodes) {
if ("outerHTML" in node) {
nodeCode.push(node.outerHTML)
} else {
const wrap = document.createElement('div');
wrap.appendChild(node.cloneNode(true));
nodeCode.push(wrap.innerHTML);
}
}

return nodeCode.join('\n');
}

function $appendNodesToHTML(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,18 +273,6 @@ describe('LexicalAutoAutoLinkNode tests', () => {
});
});

test('AutoLinkNode.createDOM() sanitizes javascript: URLs', async () => {
const {editor} = testEnv;

await editor.update(() => {
// eslint-disable-next-line no-script-url
const autoLinkNode = new AutoLinkNode('javascript:alert(0)');
expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
'<a href="about:blank" class="my-autolink-class"></a>',
);
});
});

test('AutoLinkNode.updateDOM()', async () => {
const {editor} = testEnv;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,18 +218,6 @@ describe('LexicalLinkNode tests', () => {
});
});

test('LinkNode.createDOM() sanitizes javascript: URLs', async () => {
const {editor} = testEnv;

await editor.update(() => {
// eslint-disable-next-line no-script-url
const linkNode = new LinkNode('javascript:alert(0)');
expect(linkNode.createDOM(editorConfig).outerHTML).toBe(
'<a href="about:blank" class="my-link-class"></a>',
);
});
});

test('LinkNode.updateDOM()', async () => {
const {editor} = testEnv;

Expand Down
23 changes: 1 addition & 22 deletions resources/js/wysiwyg/lexical/link/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,6 @@ export type SerializedLinkNode = Spread<

type LinkHTMLElementType = HTMLAnchorElement | HTMLSpanElement;

const SUPPORTED_URL_PROTOCOLS = new Set([
'http:',
'https:',
'mailto:',
'sms:',
'tel:',
]);

/** @noInheritDoc */
export class LinkNode extends ElementNode {
/** @internal */
Expand Down Expand Up @@ -90,7 +82,7 @@ export class LinkNode extends ElementNode {

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;
}
Expand Down Expand Up @@ -166,19 +158,6 @@ export class LinkNode extends ElementNode {
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(),
Expand Down
9 changes: 8 additions & 1 deletion resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,10 +353,17 @@ export function $convertTableCellNodeElement(
const hasUnderlineTextDecoration = textDecoration.includes('underline');

if (domNode instanceof HTMLElement) {
tableCellNode.setStyles(extractStyleMapFromElement(domNode));
const styleMap = extractStyleMapFromElement(domNode);
styleMap.delete('background-color');
tableCellNode.setStyles(styleMap);
tableCellNode.setAlignment(extractAlignmentFromElement(domNode));
}

const background = style.backgroundColor || null;
if (background) {
tableCellNode.setBackgroundColor(background);
}

return {
after: (childLexicalNodes) => {
if (childLexicalNodes.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,31 @@ describe('LexicalUtils#splitNode', () => {
{
_: 'split paragraph in between two text nodes',
expectedHtml:
'<p>Hello</p><p>world</p>',
'<p>Hello</p>\n<p>world</p>',
initialHtml: '<p><span>Hello</span><span>world</span></p>',
splitOffset: 1,
splitPath: [0],
},
{
_: 'split paragraph before the first text node',
expectedHtml:
'<p><br></p><p>Helloworld</p>',
'<p><br></p>\n<p>Helloworld</p>',
initialHtml: '<p><span>Hello</span><span>world</span></p>',
splitOffset: 0,
splitPath: [0],
},
{
_: 'split paragraph after the last text node',
expectedHtml:
'<p>Helloworld</p><p><br></p>',
'<p>Helloworld</p>\n<p><br></p>',
initialHtml: '<p><span>Hello</span><span>world</span></p>',
splitOffset: 2, // Any offset that is higher than children size
splitPath: [0],
},
{
_: 'split list items between two text nodes',
expectedHtml:
'<ul><li>Hello</li></ul>' +
'<ul><li>Hello</li></ul>\n' +
'<ul><li>world</li></ul>',
initialHtml: '<ul><li><span>Hello</span><span>world</span></li></ul>',
splitOffset: 1, // Any offset that is higher than children size
Expand All @@ -71,7 +71,7 @@ describe('LexicalUtils#splitNode', () => {
{
_: 'split list items before the first text node',
expectedHtml:
'<ul><li></li></ul>' +
'<ul><li></li></ul>\n' +
'<ul><li>Helloworld</li></ul>',
initialHtml: '<ul><li><span>Hello</span><span>world</span></li></ul>',
splitOffset: 0, // Any offset that is higher than children size
Expand All @@ -83,7 +83,7 @@ describe('LexicalUtils#splitNode', () => {
'<ul>' +
'<li>Before</li>' +
'<li style="list-style: none;"><ul><li>Hello</li></ul></li>' +
'</ul>' +
'</ul>\n' +
'<ul>' +
'<li style="list-style: none;"><ul><li>world</li></ul></li>' +
'<li>After</li>' +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
{
_: 'insert into paragraph in between two text nodes',
expectedHtml:
'<p>Hello</p><test-decorator></test-decorator><p>world</p>',
'<p>Hello</p>\n<test-decorator></test-decorator>\n<p>world</p>',
initialHtml: '<p><span>Helloworld</span></p>',
selectionOffset: 5, // Selection on text node after "Hello" world
selectionPath: [0, 0],
Expand All @@ -57,8 +57,8 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
'<ul>' +
'<li>Before</li>' +
'<li style="list-style: none;"><ul><li>Hello</li></ul></li>' +
'</ul>' +
'<test-decorator></test-decorator>' +
'</ul>\n' +
'<test-decorator></test-decorator>\n' +
'<ul>' +
'<li style="list-style: none;"><ul><li>world</li></ul></li>' +
'<li>After</li>' +
Expand All @@ -74,16 +74,16 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
},
{
_: 'insert into empty paragraph',
expectedHtml: '<p><br></p><test-decorator></test-decorator><p><br></p>',
expectedHtml: '<p><br></p>\n<test-decorator></test-decorator>\n<p><br></p>',
initialHtml: '<p></p>',
selectionOffset: 0, // Selection on text node after "Hello" world
selectionPath: [0],
},
{
_: 'insert in the end of paragraph',
expectedHtml:
'<p>Hello world</p>' +
'<test-decorator></test-decorator>' +
'<p>Hello world</p>\n' +
'<test-decorator></test-decorator>\n' +
'<p><br></p>',
initialHtml: '<p>Hello world</p>',
selectionOffset: 12, // Selection on text node after "Hello" world
Expand All @@ -92,8 +92,8 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
{
_: 'insert in the beginning of paragraph',
expectedHtml:
'<p><br></p>' +
'<test-decorator></test-decorator>' +
'<p><br></p>\n' +
'<test-decorator></test-decorator>\n' +
'<p>Hello world</p>',
initialHtml: '<p>Hello world</p>',
selectionOffset: 0, // Selection on text node after "Hello" world
Expand All @@ -102,9 +102,9 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
{
_: 'insert with selection on root start',
expectedHtml:
'<test-decorator></test-decorator>' +
'<test-decorator></test-decorator>' +
'<p>Before</p>' +
'<test-decorator></test-decorator>\n' +
'<test-decorator></test-decorator>\n' +
'<p>Before</p>\n' +
'<p>After</p>',
initialHtml:
'<test-decorator></test-decorator>' +
Expand All @@ -116,8 +116,8 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
{
_: 'insert with selection on root child',
expectedHtml:
'<p>Before</p>' +
'<test-decorator></test-decorator>' +
'<p>Before</p>\n' +
'<test-decorator></test-decorator>\n' +
'<p>After</p>',
initialHtml: '<p>Before</p><p>After</p>',
selectionOffset: 1,
Expand All @@ -126,7 +126,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
{
_: 'insert with selection on root end',
expectedHtml:
'<p>Before</p>' +
'<p>Before</p>\n' +
'<test-decorator></test-decorator>',
initialHtml: '<p>Before</p>',
selectionOffset: 1,
Expand Down
51 changes: 51 additions & 0 deletions resources/js/wysiwyg/services/__tests__/mouse-handling.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading