Skip to content

Further Lexical Fixes #5415

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 9 commits into from
Feb 16, 2025
2 changes: 2 additions & 0 deletions lang/en/editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
'cancel' => 'Cancel',
'save' => 'Save',
'close' => 'Close',
'apply' => 'Apply',
'undo' => 'Undo',
'redo' => 'Redo',
'left' => 'Left',
Expand Down Expand Up @@ -147,6 +148,7 @@
'url' => 'URL',
'text_to_display' => 'Text to display',
'title' => 'Title',
'browse_links' => 'Browse links',
'open_link' => 'Open link',
'open_link_in' => 'Open link in...',
'open_link_current' => 'Current window',
Expand Down
10 changes: 10 additions & 0 deletions resources/icons/editor/color-display.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/icons/editor/color-select.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 0 additions & 3 deletions resources/js/wysiwyg/lexical/core/LexicalCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import type {
BaseSelection,
ElementFormatType,
LexicalCommand,
LexicalNode,
TextFormatType,
Expand Down Expand Up @@ -91,8 +90,6 @@ export const OUTDENT_CONTENT_COMMAND: LexicalCommand<void> = createCommand(
);
export const DROP_COMMAND: LexicalCommand<DragEvent> =
createCommand('DROP_COMMAND');
export const FORMAT_ELEMENT_COMMAND: LexicalCommand<ElementFormatType> =
createCommand('FORMAT_ELEMENT_COMMAND');
export const DRAGSTART_COMMAND: LexicalCommand<DragEvent> =
createCommand('DRAGSTART_COMMAND');
export const DRAGOVER_COMMAND: LexicalCommand<DragEvent> =
Expand Down
22 changes: 0 additions & 22 deletions resources/js/wysiwyg/lexical/core/LexicalConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*
*/

import type {ElementFormatType} from './nodes/LexicalElementNode';
import type {
TextDetailType,
TextFormatType,
Expand Down Expand Up @@ -111,27 +110,6 @@ export const DETAIL_TYPE_TO_DETAIL: Record<TextDetailType | string, number> = {
unmergeable: IS_UNMERGEABLE,
};

export const ELEMENT_TYPE_TO_FORMAT: Record<
Exclude<ElementFormatType, ''>,
number
> = {
center: IS_ALIGN_CENTER,
end: IS_ALIGN_END,
justify: IS_ALIGN_JUSTIFY,
left: IS_ALIGN_LEFT,
right: IS_ALIGN_RIGHT,
start: IS_ALIGN_START,
};

export const ELEMENT_FORMAT_TO_TYPE: Record<number, ElementFormatType> = {
[IS_ALIGN_CENTER]: 'center',
[IS_ALIGN_END]: 'end',
[IS_ALIGN_JUSTIFY]: 'justify',
[IS_ALIGN_LEFT]: 'left',
[IS_ALIGN_RIGHT]: 'right',
[IS_ALIGN_START]: 'start',
};

export const TEXT_MODE_TO_TYPE: Record<TextModeType, 0 | 1 | 2> = {
normal: IS_NORMAL,
segmented: IS_SEGMENTED,
Expand Down
16 changes: 16 additions & 0 deletions resources/js/wysiwyg/lexical/core/LexicalNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ type NodeName = string;
* Output for a DOM conversion.
* Node can be set to 'ignore' to ignore the conversion and handling of the DOMNode
* including all its children.
*
* You can specify a function to run for each converted child (forChild) or on all
* the child nodes after the conversion is complete (after).
* The key difference here is that forChild runs for every deeply nested child node
* of the current node, whereas after will run only once after the
* transformation of the node and all its children is complete.
*/
export type DOMConversionOutput = {
after?: (childLexicalNodes: Array<LexicalNode>) => Array<LexicalNode>;
Expand Down Expand Up @@ -1165,6 +1171,16 @@ export class LexicalNode {
markDirty(): void {
this.getWritable();
}

/**
* Insert the DOM of this node into that of the parent.
* Allows this node to implement custom DOM attachment logic.
* Boolean result indicates if the insertion was handled by the function.
* A true return value prevents default insertion logic from taking place.
*/
insertDOMIntoParent(nodeDOM: HTMLElement, parentDOM: HTMLElement): boolean {
return false;
}
}

function errorOnTypeKlassMismatch(
Expand Down
21 changes: 13 additions & 8 deletions resources/js/wysiwyg/lexical/core/LexicalReconciler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,16 +171,21 @@ function $createNode(
}

if (parentDOM !== null) {
if (insertDOM != null) {
parentDOM.insertBefore(dom, insertDOM);
} else {
// @ts-expect-error: internal field
const possibleLineBreak = parentDOM.__lexicalLineBreak;

if (possibleLineBreak != null) {
parentDOM.insertBefore(dom, possibleLineBreak);
const inserted = node?.insertDOMIntoParent(dom, parentDOM);

if (!inserted) {
if (insertDOM != null) {
parentDOM.insertBefore(dom, insertDOM);
} else {
parentDOM.appendChild(dom);
// @ts-expect-error: internal field
const possibleLineBreak = parentDOM.__lexicalLineBreak;

if (possibleLineBreak != null) {
parentDOM.insertBefore(dom, possibleLineBreak);
} else {
parentDOM.appendChild(dom);
}
}
}
}
Expand Down
29 changes: 27 additions & 2 deletions resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
import {DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
import {EditorUiContext} from "../../../../ui/framework/core";
import {EditorUIManager} from "../../../../ui/framework/manager";
import {turtle} from "@codemirror/legacy-modes/mode/turtle";


type TestEnv = {
readonly container: HTMLDivElement;
Expand All @@ -47,6 +45,9 @@ type TestEnv = {
readonly innerHTML: string;
};

/**
* @deprecated - Consider using `createTestContext` instead within the test case.
*/
export function initializeUnitTest(
runTests: (testEnv: TestEnv) => void,
editorConfig: CreateEditorArgs = {namespace: 'test', theme: {}},
Expand Down Expand Up @@ -795,6 +796,30 @@ export function expectNodeShapeToMatch(editor: LexicalEditor, expected: nodeShap
expect(shape.children).toMatchObject(expected);
}

/**
* Expect a given prop within the JSON editor state structure to be the given value.
* Uses dot notation for the provided `propPath`. Example:
* 0.5.cat => First child, Sixth child, cat property
*/
export function expectEditorStateJSONPropToEqual(editor: LexicalEditor, propPath: string, expected: any) {
let currentItem: any = editor.getEditorState().toJSON().root;
let currentPath = [];
const pathParts = propPath.split('.');

for (const part of pathParts) {
currentPath.push(part);
const childAccess = Number.isInteger(Number(part)) && Array.isArray(currentItem.children);
const target = childAccess ? currentItem.children : currentItem;

if (typeof target[part] === 'undefined') {
throw new Error(`Could not resolve editor state at path ${currentPath.join('.')}`)
}
currentItem = target[part];
}

expect(currentItem).toBe(expected);
}

function formatHtml(s: string): string {
return s.replace(/>\s+</g, '><').replace(/\s*\n\s*/g, ' ').trim();
}
Expand Down
4 changes: 0 additions & 4 deletions resources/js/wysiwyg/lexical/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,12 @@ export type {
} from './LexicalNode';
export type {
BaseSelection,
ElementPointType as ElementPoint,
NodeSelection,
Point,
PointType,
RangeSelection,
TextPointType as TextPoint,
} from './LexicalSelection';
export type {
ElementFormatType,
SerializedElementNode,
} from './nodes/LexicalElementNode';
export type {SerializedRootNode} from './nodes/LexicalRootNode';
Expand Down Expand Up @@ -87,7 +84,6 @@ export {
DRAGSTART_COMMAND,
DROP_COMMAND,
FOCUS_COMMAND,
FORMAT_ELEMENT_COMMAND,
FORMAT_TEXT_COMMAND,
INDENT_CONTENT_COMMAND,
INSERT_LINE_BREAK_COMMAND,
Expand Down
9 changes: 0 additions & 9 deletions resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,6 @@ export type SerializedElementNode<
SerializedLexicalNode
>;

export type ElementFormatType =
| 'left'
| 'start'
| 'center'
| 'right'
| 'end'
| 'justify'
| '';

// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export interface ElementNode {
getTopLevelElement(): ElementNode | null;
Expand Down
5 changes: 5 additions & 0 deletions resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,11 @@ const nodeNameToTextFormat: Record<string, TextFormatType> = {

function convertTextFormatElement(domNode: HTMLElement): DOMConversionOutput {
const format = nodeNameToTextFormat[domNode.nodeName.toLowerCase()];

if (format === 'code' && domNode.closest('pre')) {
return {node: null};
}

if (format === undefined) {
return {node: null};
}
Expand Down
10 changes: 5 additions & 5 deletions resources/js/wysiwyg/lexical/core/shared/invariant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ export default function invariant(
return;
}

throw new Error(
'Internal Lexical error: invariant() is meant to be replaced at compile ' +
'time. There is no runtime version. Error: ' +
message,
);
for (const arg of args) {
message = (message || '').replace('%s', arg);
}

throw new Error(message);
}
4 changes: 1 addition & 3 deletions resources/js/wysiwyg/lexical/html/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type {
DOMChildConversion,
DOMConversion,
DOMConversionFn,
ElementFormatType,
LexicalEditor,
LexicalNode,
} from 'lexical';
Expand Down Expand Up @@ -58,6 +57,7 @@ export function $generateNodesFromDOM(
}
}
}

$unwrapArtificalNodes(allArtificialNodes);

return lexicalNodes;
Expand Down Expand Up @@ -324,8 +324,6 @@ function wrapContinuousInlines(
nodes: Array<LexicalNode>,
createWrapperFn: () => ElementNode,
): Array<LexicalNode> {
const textAlign = (domNode as HTMLElement).style
.textAlign as ElementFormatType;
const out: Array<LexicalNode> = [];
let continuousInlines: Array<LexicalNode> = [];
// wrap contiguous inline child nodes in para
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,14 @@ export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
node.setId(element.id);
}

return { node };
return {
node,
after(childNodes): LexicalNode[] {
// Remove any child nodes that may get parsed since we're manually
// controlling the code contents.
return [];
},
};
},
priority: 3,
};
Expand Down
49 changes: 48 additions & 1 deletion resources/js/wysiwyg/lexical/rich-text/LexicalMediaNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "lexical/nodes/common";
import {$selectSingleNode} from "../../utils/selection";
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
import * as url from "node:url";

export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';
export type MediaNodeSource = {
Expand Down Expand Up @@ -343,11 +344,55 @@ export function $createMediaNodeFromHtml(html: string): MediaNode | null {
return domElementToNode(tag as MediaNodeTag, el);
}

interface UrlPattern {
readonly regex: RegExp;
readonly w: number;
readonly h: number;
readonly url: string;
}

/**
* These patterns originate from the tinymce/tinymce project.
* https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts
* License: MIT Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.
* License Link: https://github.com/tinymce/tinymce/blob/584a150679669859a528828e5d2910a083b1d911/LICENSE.TXT
*/
const urlPatterns: UrlPattern[] = [
{
regex: /.*?youtu\.be\/([\w\-_\?&=.]+)/i,
w: 560, h: 314,
url: 'https://www.youtube.com/embed/$1',
},
{
regex: /.*youtube\.com(.+)v=([^&]+)(&([a-z0-9&=\-_]+))?.*/i,
w: 560, h: 314,
url: 'https://www.youtube.com/embed/$2?$4',
},
{
regex: /.*youtube.com\/embed\/([a-z0-9\?&=\-_]+).*/i,
w: 560, h: 314,
url: 'https://www.youtube.com/embed/$1',
},
];

const videoExtensions = ['mp4', 'mpeg', 'm4v', 'm4p', 'mov'];
const audioExtensions = ['3gp', 'aac', 'flac', 'mp3', 'm4a', 'ogg', 'wav', 'webm'];
const iframeExtensions = ['html', 'htm', 'php', 'asp', 'aspx', ''];

export function $createMediaNodeFromSrc(src: string): MediaNode {

for (const pattern of urlPatterns) {
const match = src.match(pattern.regex);
if (match) {
const newSrc = src.replace(pattern.regex, pattern.url);
const node = new MediaNode('iframe');
node.setSrc(newSrc);
node.setHeight(pattern.h);
node.setWidth(pattern.w);
return node;
}
}

let nodeTag: MediaNodeTag = 'iframe';
const srcEnd = src.split('?')[0].split('/').pop() || '';
const srcEndSplit = srcEnd.split('.');
Expand All @@ -360,7 +405,9 @@ export function $createMediaNodeFromSrc(src: string): MediaNode {
nodeTag = 'embed';
}

return new MediaNode(nodeTag);
const node = new MediaNode(nodeTag);
node.setSrc(src);
return node;
}

export function $isMediaNode(node: LexicalNode | null | undefined): node is MediaNode {
Expand Down
Loading
Loading