Skip to content

Commit c8cfec9

Browse files
authored
Merge pull request #5731 from BookStackApp/lexical_jul25
New WYSIWYG editor changes for July 2025
2 parents 0a73b70 + d145efb commit c8cfec9

File tree

18 files changed

+197
-81
lines changed

18 files changed

+197
-81
lines changed

lang/en/editor.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
'superscript' => 'Superscript',
4949
'subscript' => 'Subscript',
5050
'text_color' => 'Text color',
51+
'highlight_color' => 'Highlight color',
5152
'custom_color' => 'Custom color',
5253
'remove_color' => 'Remove color',
5354
'background_color' => 'Background color',

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/html/__tests__/unit/LexicalHtml.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ describe('HTML', () => {
146146
});
147147

148148
expect(html).toBe(
149-
'<p>Hello</p><p>World</p>',
149+
'<p>Hello</p>\n<p>World</p>',
150150
);
151151
});
152152

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,18 @@ export function $generateHtmlFromNodes(
8585
$appendNodesToHTML(editor, topLevelNode, container, selection);
8686
}
8787

88-
return container.innerHTML;
88+
const nodeCode = [];
89+
for (const node of container.childNodes) {
90+
if ("outerHTML" in node) {
91+
nodeCode.push(node.outerHTML)
92+
} else {
93+
const wrap = document.createElement('div');
94+
wrap.appendChild(node.cloneNode(true));
95+
nodeCode.push(wrap.innerHTML);
96+
}
97+
}
98+
99+
return nodeCode.join('\n');
89100
}
90101

91102
function $appendNodesToHTML(

resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalAutoLinkNode.test.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -273,18 +273,6 @@ describe('LexicalAutoAutoLinkNode tests', () => {
273273
});
274274
});
275275

276-
test('AutoLinkNode.createDOM() sanitizes javascript: URLs', async () => {
277-
const {editor} = testEnv;
278-
279-
await editor.update(() => {
280-
// eslint-disable-next-line no-script-url
281-
const autoLinkNode = new AutoLinkNode('javascript:alert(0)');
282-
expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
283-
'<a href="about:blank" class="my-autolink-class"></a>',
284-
);
285-
});
286-
});
287-
288276
test('AutoLinkNode.updateDOM()', async () => {
289277
const {editor} = testEnv;
290278

resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalLinkNode.test.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -218,18 +218,6 @@ describe('LexicalLinkNode tests', () => {
218218
});
219219
});
220220

221-
test('LinkNode.createDOM() sanitizes javascript: URLs', async () => {
222-
const {editor} = testEnv;
223-
224-
await editor.update(() => {
225-
// eslint-disable-next-line no-script-url
226-
const linkNode = new LinkNode('javascript:alert(0)');
227-
expect(linkNode.createDOM(editorConfig).outerHTML).toBe(
228-
'<a href="about:blank" class="my-link-class"></a>',
229-
);
230-
});
231-
});
232-
233221
test('LinkNode.updateDOM()', async () => {
234222
const {editor} = testEnv;
235223

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(),

resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,10 +353,17 @@ export function $convertTableCellNodeElement(
353353
const hasUnderlineTextDecoration = textDecoration.includes('underline');
354354

355355
if (domNode instanceof HTMLElement) {
356-
tableCellNode.setStyles(extractStyleMapFromElement(domNode));
356+
const styleMap = extractStyleMapFromElement(domNode);
357+
styleMap.delete('background-color');
358+
tableCellNode.setStyles(styleMap);
357359
tableCellNode.setAlignment(extractAlignmentFromElement(domNode));
358360
}
359361

362+
const background = style.backgroundColor || null;
363+
if (background) {
364+
tableCellNode.setBackgroundColor(background);
365+
}
366+
360367
return {
361368
after: (childLexicalNodes) => {
362369
if (childLexicalNodes.length === 0) {

resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalUtilsSplitNode.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,31 +38,31 @@ describe('LexicalUtils#splitNode', () => {
3838
{
3939
_: 'split paragraph in between two text nodes',
4040
expectedHtml:
41-
'<p>Hello</p><p>world</p>',
41+
'<p>Hello</p>\n<p>world</p>',
4242
initialHtml: '<p><span>Hello</span><span>world</span></p>',
4343
splitOffset: 1,
4444
splitPath: [0],
4545
},
4646
{
4747
_: 'split paragraph before the first text node',
4848
expectedHtml:
49-
'<p><br></p><p>Helloworld</p>',
49+
'<p><br></p>\n<p>Helloworld</p>',
5050
initialHtml: '<p><span>Hello</span><span>world</span></p>',
5151
splitOffset: 0,
5252
splitPath: [0],
5353
},
5454
{
5555
_: 'split paragraph after the last text node',
5656
expectedHtml:
57-
'<p>Helloworld</p><p><br></p>',
57+
'<p>Helloworld</p>\n<p><br></p>',
5858
initialHtml: '<p><span>Hello</span><span>world</span></p>',
5959
splitOffset: 2, // Any offset that is higher than children size
6060
splitPath: [0],
6161
},
6262
{
6363
_: 'split list items between two text nodes',
6464
expectedHtml:
65-
'<ul><li>Hello</li></ul>' +
65+
'<ul><li>Hello</li></ul>\n' +
6666
'<ul><li>world</li></ul>',
6767
initialHtml: '<ul><li><span>Hello</span><span>world</span></li></ul>',
6868
splitOffset: 1, // Any offset that is higher than children size
@@ -71,7 +71,7 @@ describe('LexicalUtils#splitNode', () => {
7171
{
7272
_: 'split list items before the first text node',
7373
expectedHtml:
74-
'<ul><li></li></ul>' +
74+
'<ul><li></li></ul>\n' +
7575
'<ul><li>Helloworld</li></ul>',
7676
initialHtml: '<ul><li><span>Hello</span><span>world</span></li></ul>',
7777
splitOffset: 0, // Any offset that is higher than children size
@@ -83,7 +83,7 @@ describe('LexicalUtils#splitNode', () => {
8383
'<ul>' +
8484
'<li>Before</li>' +
8585
'<li style="list-style: none;"><ul><li>Hello</li></ul></li>' +
86-
'</ul>' +
86+
'</ul>\n' +
8787
'<ul>' +
8888
'<li style="list-style: none;"><ul><li>world</li></ul></li>' +
8989
'<li>After</li>' +

0 commit comments

Comments
 (0)