Skip to content

Lexical Fixes for v25.05.1 #5653

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 17, 2025
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
2 changes: 1 addition & 1 deletion resources/js/wysiwyg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st

// @ts-ignore
window.debugEditorState = () => {
console.log(editor.getEditorState().toJSON());
return editor.getEditorState().toJSON();
};

registerCommonNodeMutationListeners(context);
Expand Down
1 change: 1 addition & 0 deletions resources/js/wysiwyg/lexical/core/LexicalEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ function onSelectionChange(
lastNode instanceof ParagraphNode &&
lastNode.getChildrenSize() === 0
) {
selection.format = lastNode.getTextFormat();
selection.style = lastNode.getTextStyle();
} else {
selection.format = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,7 @@ describe('LexicalEditor tests', () => {
__prev: null,
__size: 1,
__style: '',
__textFormat: 0,
__textStyle: '',
__type: 'paragraph',
});
Expand Down Expand Up @@ -1149,6 +1150,7 @@ describe('LexicalEditor tests', () => {
__prev: null,
__size: 1,
__style: '',
__textFormat: 0,
__textStyle: '',
__type: 'paragraph',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ describe('LexicalEditorState tests', () => {
__prev: null,
__size: 1,
__style: '',
__textFormat: 0,
__textStyle: '',
__type: 'paragraph',
});
Expand Down Expand Up @@ -111,7 +112,7 @@ describe('LexicalEditorState tests', () => {
});

expect(JSON.stringify(editor.getEditorState().toJSON())).toEqual(
`{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Hello world","type":"text","version":1}],"direction":null,"type":"paragraph","version":1,"id":"","alignment":"","inset":0,"textStyle":""}],"direction":null,"type":"root","version":1}}`,
`{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Hello world","type":"text","version":1}],"direction":null,"type":"paragraph","version":1,"id":"","alignment":"","inset":0,"textFormat":0,"textStyle":""}],"direction":null,"type":"root","version":1}}`,
);
});

Expand Down

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
import {EditorUiContext} from "../../../../ui/framework/core";
import {EditorUIManager} from "../../../../ui/framework/manager";
import {ImageNode} from "@lexical/rich-text/LexicalImageNode";
import {MediaNode} from "@lexical/rich-text/LexicalMediaNode";

type TestEnv = {
readonly container: HTMLDivElement;
Expand Down Expand Up @@ -487,6 +488,7 @@ export function createTestContext(): EditorUiContext {
theme: {},
nodes: [
ImageNode,
MediaNode,
]
});

Expand Down
25 changes: 24 additions & 1 deletion resources/js/wysiwyg/lexical/core/nodes/LexicalParagraphNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {
LexicalNode,
NodeKey,
} from '../LexicalNode';
import type {RangeSelection} from 'lexical';
import {RangeSelection, TEXT_TYPE_TO_FORMAT, TextFormatType} from 'lexical';

import {
$applyNodeReplacement,
Expand All @@ -36,6 +36,7 @@ import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} f

export type SerializedParagraphNode = Spread<
{
textFormat: number;
textStyle: string;
},
SerializedCommonBlockNode
Expand All @@ -45,17 +46,35 @@ export type SerializedParagraphNode = Spread<
export class ParagraphNode extends CommonBlockNode {
['constructor']!: KlassConstructor<typeof ParagraphNode>;
/** @internal */
__textFormat: number;
__textStyle: string;

constructor(key?: NodeKey) {
super(key);
this.__textFormat = 0;
this.__textStyle = '';
}

static getType(): string {
return 'paragraph';
}

getTextFormat(): number {
const self = this.getLatest();
return self.__textFormat;
}

setTextFormat(type: number): this {
const self = this.getWritable();
self.__textFormat = type;
return self;
}

hasTextFormat(type: TextFormatType): boolean {
const formatFlag = TEXT_TYPE_TO_FORMAT[type];
return (this.getTextFormat() & formatFlag) !== 0;
}

getTextStyle(): string {
const self = this.getLatest();
return self.__textStyle;
Expand All @@ -73,6 +92,7 @@ export class ParagraphNode extends CommonBlockNode {

afterCloneFrom(prevNode: this) {
super.afterCloneFrom(prevNode);
this.__textFormat = prevNode.__textFormat;
this.__textStyle = prevNode.__textStyle;
copyCommonBlockProperties(prevNode, this);
}
Expand Down Expand Up @@ -125,12 +145,14 @@ export class ParagraphNode extends CommonBlockNode {
static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode {
const node = $createParagraphNode();
deserializeCommonBlockNode(serializedNode, node);
node.setTextFormat(serializedNode.textFormat);
return node;
}

exportJSON(): SerializedParagraphNode {
return {
...super.exportJSON(),
textFormat: this.getTextFormat(),
textStyle: this.getTextStyle(),
type: 'paragraph',
version: 1,
Expand All @@ -144,6 +166,7 @@ export class ParagraphNode extends CommonBlockNode {
restoreSelection: boolean,
): ParagraphNode {
const newElement = $createParagraphNode();
newElement.setTextFormat(rangeSelection.format);
newElement.setTextStyle(rangeSelection.style);
const direction = this.getDirection();
newElement.setDirection(direction);
Expand Down
5 changes: 3 additions & 2 deletions resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ export class TextNode extends LexicalNode {
// HTML content and not have the ability to use CSS classes.
exportDOM(editor: LexicalEditor): DOMExportOutput {
let {element} = super.exportDOM(editor);
const originalElementName = (element?.nodeName || '').toLowerCase()
invariant(
element !== null && isHTMLElement(element),
'Expected TextNode createDOM to always return a HTMLElement',
Expand Down Expand Up @@ -649,8 +650,8 @@ export class TextNode extends LexicalNode {
// This is the only way to properly add support for most clients,
// even if it's semantically incorrect to have to resort to using
// <b>, <u>, <s>, <i> elements.
if (this.hasFormat('bold')) {
element = wrapElementWith(element, 'b');
if (this.hasFormat('bold') && originalElementName !== 'strong') {
element = wrapElementWith(element, 'strong');
}
if (this.hasFormat('italic')) {
element = wrapElementWith(element, 'em');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe('LexicalParagraphNode tests', () => {
direction: null,
id: '',
inset: 0,
textFormat: 0,
textStyle: '',
type: 'paragraph',
version: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,7 @@ describe('LexicalTextNode tests', () => {
paragraph.append(textNode);

const html = $generateHtmlFromNodes($getEditor(), null);
expect(html).toBe('<p><u><em><b><code spellcheck="false"><strong>hello</strong></code></b></em></u></p>');
expect(html).toBe('<p><u><em><strong><code spellcheck="false"><strong>hello</strong></code></strong></em></u></p>');
});
});

Expand Down
55 changes: 37 additions & 18 deletions resources/js/wysiwyg/lexical/rich-text/LexicalMediaNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import {
} from 'lexical';
import type {EditorConfig} from "lexical/LexicalEditor";

import {el, setOrRemoveAttribute, sizeToPixels} from "../../utils/dom";
import {el, setOrRemoveAttribute, sizeToPixels, styleMapToStyleString, styleStringToStyleMap} from "../../utils/dom";
import {
CommonBlockAlignment, deserializeCommonBlockNode,
setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps
} from "lexical/nodes/common";
import {$selectSingleNode} from "../../utils/selection";
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";

export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';
Expand Down Expand Up @@ -46,6 +45,19 @@ function filterAttributes(attributes: Record<string, string>): Record<string, st
return filtered;
}

function removeStyleFromAttributes(attributes: Record<string, string>, styleName: string): Record<string, string> {
const attrCopy = Object.assign({}, attributes);
if (!attributes.style) {
return attrCopy;
}

const map = styleStringToStyleMap(attributes.style);
map.delete(styleName);

attrCopy.style = styleMapToStyleString(map);
return attrCopy;
}

function domElementToNode(tag: MediaNodeTag, element: HTMLElement): MediaNode {
const node = $createMediaNode(tag);

Expand Down Expand Up @@ -118,7 +130,7 @@ export class MediaNode extends ElementNode {

getAttributes(): Record<string, string> {
const self = this.getLatest();
return self.__attributes;
return Object.assign({}, self.__attributes);
}

setSources(sources: MediaNodeSource[]) {
Expand All @@ -128,25 +140,37 @@ export class MediaNode extends ElementNode {

getSources(): MediaNodeSource[] {
const self = this.getLatest();
return self.__sources;
return self.__sources.map(s => Object.assign({}, s))
}

setSrc(src: string): void {
const attrs = Object.assign({}, this.getAttributes());
const attrs = this.getAttributes();
const sources = this.getSources();

if (this.__tag ==='object') {
attrs.data = src;
} if (this.__tag === 'video' && sources.length > 0) {
sources[0].src = src;
delete attrs.src;
if (sources.length > 1) {
sources.splice(1, sources.length - 1);
}
this.setSources(sources);
} else {
attrs.src = src;
}

this.setAttributes(attrs);
}

setWidthAndHeight(width: string, height: string): void {
const attrs = Object.assign(
{},
let attrs: Record<string, string> = Object.assign(
this.getAttributes(),
{width, height},
);

attrs = removeStyleFromAttributes(attrs, 'width');
attrs = removeStyleFromAttributes(attrs, 'height');
this.setAttributes(attrs);
}

Expand Down Expand Up @@ -185,8 +209,8 @@ export class MediaNode extends ElementNode {
return;
}

const attrs = Object.assign({}, this.getAttributes(), {height});
this.setAttributes(attrs);
const attrs = Object.assign(this.getAttributes(), {height});
this.setAttributes(removeStyleFromAttributes(attrs, 'height'));
}

getHeight(): number {
Expand All @@ -195,8 +219,9 @@ export class MediaNode extends ElementNode {
}

setWidth(width: number): void {
const attrs = Object.assign({}, this.getAttributes(), {width});
this.setAttributes(attrs);
const existingAttrs = this.getAttributes();
const attrs: Record<string, string> = Object.assign(existingAttrs, {width});
this.setAttributes(removeStyleFromAttributes(attrs, 'width'));
}

getWidth(): number {
Expand All @@ -222,15 +247,9 @@ export class MediaNode extends ElementNode {

createDOM(_config: EditorConfig, _editor: LexicalEditor) {
const media = this.createInnerDOM();
const wrap = el('span', {
return el('span', {
class: media.className + ' editor-media-wrap',
}, [media]);

wrap.addEventListener('click', e => {
_editor.update(() => $selectSingleNode(this));
});

return wrap;
}

updateDOM(prevNode: MediaNode, dom: HTMLElement): boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {createTestContext} from "lexical/__tests__/utils";
import {$createMediaNode} from "@lexical/rich-text/LexicalMediaNode";


describe('LexicalMediaNode', () => {

test('setWidth/setHeight/setWidthAndHeight functions remove relevant styles', () => {
const {editor} = createTestContext();
editor.updateAndCommit(() => {
const mediaMode = $createMediaNode('video');
const defaultStyles = {style: 'width:20px;height:40px;color:red'};

mediaMode.setAttributes(defaultStyles);
mediaMode.setWidth(60);
expect(mediaMode.getWidth()).toBe(60);
expect(mediaMode.getAttributes().style).toBe('height:40px;color:red');

mediaMode.setAttributes(defaultStyles);
mediaMode.setHeight(77);
expect(mediaMode.getHeight()).toBe(77);
expect(mediaMode.getAttributes().style).toBe('width:20px;color:red');

mediaMode.setAttributes(defaultStyles);
mediaMode.setWidthAndHeight('6', '7');
expect(mediaMode.getWidth()).toBe(6);
expect(mediaMode.getHeight()).toBe(7);
expect(mediaMode.getAttributes().style).toBe('color:red');
});
});

test('setSrc on video uses sources if existing', () => {
const {editor} = createTestContext();
editor.updateAndCommit(() => {
const mediaMode = $createMediaNode('video');
mediaMode.setAttributes({src: 'z'});
mediaMode.setSources([{src: 'a', type: 'video'}, {src: 'b', type: 'video'}]);

mediaMode.setSrc('c');

expect(mediaMode.getAttributes().src).toBeUndefined();
expect(mediaMode.getSources()).toHaveLength(1);
expect(mediaMode.getSources()[0].src).toBe('c');
});
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ describe('table selection', () => {
__prev: null,
__size: 1,
__style: '',
__textFormat: 0,
__textStyle: '',
__type: 'paragraph',
});
Expand Down
1 change: 1 addition & 0 deletions resources/js/wysiwyg/ui/defaults/buttons/objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const image: EditorButtonDefinition = {
context.editor.update(() => {
const link = $createLinkedImageNodeFromImageData(image);
$insertNodes([link]);
link.select();
});
})
});
Expand Down
Loading