Skip to content

Commit 213a86e

Browse files
authored
Merge pull request #5415 from BookStackApp/more_lexical_fixes
Further Lexical Fixes
2 parents 5c15f4a + 2b74642 commit 213a86e

35 files changed

+569
-237
lines changed

lang/en/editor.php

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
'cancel' => 'Cancel',
1414
'save' => 'Save',
1515
'close' => 'Close',
16+
'apply' => 'Apply',
1617
'undo' => 'Undo',
1718
'redo' => 'Redo',
1819
'left' => 'Left',
@@ -147,6 +148,7 @@
147148
'url' => 'URL',
148149
'text_to_display' => 'Text to display',
149150
'title' => 'Title',
151+
'browse_links' => 'Browse links',
150152
'open_link' => 'Open link',
151153
'open_link_in' => 'Open link in...',
152154
'open_link_current' => 'Current window',
+10
Loading
+1
Loading

resources/js/wysiwyg/lexical/core/LexicalCommands.ts

-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import type {
1010
BaseSelection,
11-
ElementFormatType,
1211
LexicalCommand,
1312
LexicalNode,
1413
TextFormatType,
@@ -91,8 +90,6 @@ export const OUTDENT_CONTENT_COMMAND: LexicalCommand<void> = createCommand(
9190
);
9291
export const DROP_COMMAND: LexicalCommand<DragEvent> =
9392
createCommand('DROP_COMMAND');
94-
export const FORMAT_ELEMENT_COMMAND: LexicalCommand<ElementFormatType> =
95-
createCommand('FORMAT_ELEMENT_COMMAND');
9693
export const DRAGSTART_COMMAND: LexicalCommand<DragEvent> =
9794
createCommand('DRAGSTART_COMMAND');
9895
export const DRAGOVER_COMMAND: LexicalCommand<DragEvent> =

resources/js/wysiwyg/lexical/core/LexicalConstants.ts

-22
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
*
77
*/
88

9-
import type {ElementFormatType} from './nodes/LexicalElementNode';
109
import type {
1110
TextDetailType,
1211
TextFormatType,
@@ -111,27 +110,6 @@ export const DETAIL_TYPE_TO_DETAIL: Record<TextDetailType | string, number> = {
111110
unmergeable: IS_UNMERGEABLE,
112111
};
113112

114-
export const ELEMENT_TYPE_TO_FORMAT: Record<
115-
Exclude<ElementFormatType, ''>,
116-
number
117-
> = {
118-
center: IS_ALIGN_CENTER,
119-
end: IS_ALIGN_END,
120-
justify: IS_ALIGN_JUSTIFY,
121-
left: IS_ALIGN_LEFT,
122-
right: IS_ALIGN_RIGHT,
123-
start: IS_ALIGN_START,
124-
};
125-
126-
export const ELEMENT_FORMAT_TO_TYPE: Record<number, ElementFormatType> = {
127-
[IS_ALIGN_CENTER]: 'center',
128-
[IS_ALIGN_END]: 'end',
129-
[IS_ALIGN_JUSTIFY]: 'justify',
130-
[IS_ALIGN_LEFT]: 'left',
131-
[IS_ALIGN_RIGHT]: 'right',
132-
[IS_ALIGN_START]: 'start',
133-
};
134-
135113
export const TEXT_MODE_TO_TYPE: Record<TextModeType, 0 | 1 | 2> = {
136114
normal: IS_NORMAL,
137115
segmented: IS_SEGMENTED,

resources/js/wysiwyg/lexical/core/LexicalNode.ts

+16
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,12 @@ type NodeName = string;
146146
* Output for a DOM conversion.
147147
* Node can be set to 'ignore' to ignore the conversion and handling of the DOMNode
148148
* including all its children.
149+
*
150+
* You can specify a function to run for each converted child (forChild) or on all
151+
* the child nodes after the conversion is complete (after).
152+
* The key difference here is that forChild runs for every deeply nested child node
153+
* of the current node, whereas after will run only once after the
154+
* transformation of the node and all its children is complete.
149155
*/
150156
export type DOMConversionOutput = {
151157
after?: (childLexicalNodes: Array<LexicalNode>) => Array<LexicalNode>;
@@ -1165,6 +1171,16 @@ export class LexicalNode {
11651171
markDirty(): void {
11661172
this.getWritable();
11671173
}
1174+
1175+
/**
1176+
* Insert the DOM of this node into that of the parent.
1177+
* Allows this node to implement custom DOM attachment logic.
1178+
* Boolean result indicates if the insertion was handled by the function.
1179+
* A true return value prevents default insertion logic from taking place.
1180+
*/
1181+
insertDOMIntoParent(nodeDOM: HTMLElement, parentDOM: HTMLElement): boolean {
1182+
return false;
1183+
}
11681184
}
11691185

11701186
function errorOnTypeKlassMismatch(

resources/js/wysiwyg/lexical/core/LexicalReconciler.ts

+13-8
Original file line numberDiff line numberDiff line change
@@ -171,16 +171,21 @@ function $createNode(
171171
}
172172

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

180-
if (possibleLineBreak != null) {
181-
parentDOM.insertBefore(dom, possibleLineBreak);
175+
const inserted = node?.insertDOMIntoParent(dom, parentDOM);
176+
177+
if (!inserted) {
178+
if (insertDOM != null) {
179+
parentDOM.insertBefore(dom, insertDOM);
182180
} else {
183-
parentDOM.appendChild(dom);
181+
// @ts-expect-error: internal field
182+
const possibleLineBreak = parentDOM.__lexicalLineBreak;
183+
184+
if (possibleLineBreak != null) {
185+
parentDOM.insertBefore(dom, possibleLineBreak);
186+
} else {
187+
parentDOM.appendChild(dom);
188+
}
184189
}
185190
}
186191
}

resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts

+27-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
3737
import {DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
3838
import {EditorUiContext} from "../../../../ui/framework/core";
3939
import {EditorUIManager} from "../../../../ui/framework/manager";
40-
import {turtle} from "@codemirror/legacy-modes/mode/turtle";
41-
4240

4341
type TestEnv = {
4442
readonly container: HTMLDivElement;
@@ -47,6 +45,9 @@ type TestEnv = {
4745
readonly innerHTML: string;
4846
};
4947

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

799+
/**
800+
* Expect a given prop within the JSON editor state structure to be the given value.
801+
* Uses dot notation for the provided `propPath`. Example:
802+
* 0.5.cat => First child, Sixth child, cat property
803+
*/
804+
export function expectEditorStateJSONPropToEqual(editor: LexicalEditor, propPath: string, expected: any) {
805+
let currentItem: any = editor.getEditorState().toJSON().root;
806+
let currentPath = [];
807+
const pathParts = propPath.split('.');
808+
809+
for (const part of pathParts) {
810+
currentPath.push(part);
811+
const childAccess = Number.isInteger(Number(part)) && Array.isArray(currentItem.children);
812+
const target = childAccess ? currentItem.children : currentItem;
813+
814+
if (typeof target[part] === 'undefined') {
815+
throw new Error(`Could not resolve editor state at path ${currentPath.join('.')}`)
816+
}
817+
currentItem = target[part];
818+
}
819+
820+
expect(currentItem).toBe(expected);
821+
}
822+
798823
function formatHtml(s: string): string {
799824
return s.replace(/>\s+</g, '><').replace(/\s*\n\s*/g, ' ').trim();
800825
}

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

-4
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,12 @@ export type {
4949
} from './LexicalNode';
5050
export type {
5151
BaseSelection,
52-
ElementPointType as ElementPoint,
5352
NodeSelection,
5453
Point,
5554
PointType,
5655
RangeSelection,
57-
TextPointType as TextPoint,
5856
} from './LexicalSelection';
5957
export type {
60-
ElementFormatType,
6158
SerializedElementNode,
6259
} from './nodes/LexicalElementNode';
6360
export type {SerializedRootNode} from './nodes/LexicalRootNode';
@@ -87,7 +84,6 @@ export {
8784
DRAGSTART_COMMAND,
8885
DROP_COMMAND,
8986
FOCUS_COMMAND,
90-
FORMAT_ELEMENT_COMMAND,
9187
FORMAT_TEXT_COMMAND,
9288
INDENT_CONTENT_COMMAND,
9389
INSERT_LINE_BREAK_COMMAND,

resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts

-9
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,6 @@ export type SerializedElementNode<
4646
SerializedLexicalNode
4747
>;
4848

49-
export type ElementFormatType =
50-
| 'left'
51-
| 'start'
52-
| 'center'
53-
| 'right'
54-
| 'end'
55-
| 'justify'
56-
| '';
57-
5849
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
5950
export interface ElementNode {
6051
getTopLevelElement(): ElementNode | null;

resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1314,6 +1314,11 @@ const nodeNameToTextFormat: Record<string, TextFormatType> = {
13141314

13151315
function convertTextFormatElement(domNode: HTMLElement): DOMConversionOutput {
13161316
const format = nodeNameToTextFormat[domNode.nodeName.toLowerCase()];
1317+
1318+
if (format === 'code' && domNode.closest('pre')) {
1319+
return {node: null};
1320+
}
1321+
13171322
if (format === undefined) {
13181323
return {node: null};
13191324
}

resources/js/wysiwyg/lexical/core/shared/invariant.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ export default function invariant(
1818
return;
1919
}
2020

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

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

+1-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type {
1111
DOMChildConversion,
1212
DOMConversion,
1313
DOMConversionFn,
14-
ElementFormatType,
1514
LexicalEditor,
1615
LexicalNode,
1716
} from 'lexical';
@@ -58,6 +57,7 @@ export function $generateNodesFromDOM(
5857
}
5958
}
6059
}
60+
6161
$unwrapArtificalNodes(allArtificialNodes);
6262

6363
return lexicalNodes;
@@ -324,8 +324,6 @@ function wrapContinuousInlines(
324324
nodes: Array<LexicalNode>,
325325
createWrapperFn: () => ElementNode,
326326
): Array<LexicalNode> {
327-
const textAlign = (domNode as HTMLElement).style
328-
.textAlign as ElementFormatType;
329327
const out: Array<LexicalNode> = [];
330328
let continuousInlines: Array<LexicalNode> = [];
331329
// wrap contiguous inline child nodes in para

resources/js/wysiwyg/lexical/rich-text/LexicalCodeBlockNode.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,14 @@ export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
145145
node.setId(element.id);
146146
}
147147

148-
return { node };
148+
return {
149+
node,
150+
after(childNodes): LexicalNode[] {
151+
// Remove any child nodes that may get parsed since we're manually
152+
// controlling the code contents.
153+
return [];
154+
},
155+
};
149156
},
150157
priority: 3,
151158
};

resources/js/wysiwyg/lexical/rich-text/LexicalMediaNode.ts

+48-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from "lexical/nodes/common";
1717
import {$selectSingleNode} from "../../utils/selection";
1818
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
19+
import * as url from "node:url";
1920

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

347+
interface UrlPattern {
348+
readonly regex: RegExp;
349+
readonly w: number;
350+
readonly h: number;
351+
readonly url: string;
352+
}
353+
354+
/**
355+
* These patterns originate from the tinymce/tinymce project.
356+
* https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts
357+
* License: MIT Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.
358+
* License Link: https://github.com/tinymce/tinymce/blob/584a150679669859a528828e5d2910a083b1d911/LICENSE.TXT
359+
*/
360+
const urlPatterns: UrlPattern[] = [
361+
{
362+
regex: /.*?youtu\.be\/([\w\-_\?&=.]+)/i,
363+
w: 560, h: 314,
364+
url: 'https://www.youtube.com/embed/$1',
365+
},
366+
{
367+
regex: /.*youtube\.com(.+)v=([^&]+)(&([a-z0-9&=\-_]+))?.*/i,
368+
w: 560, h: 314,
369+
url: 'https://www.youtube.com/embed/$2?$4',
370+
},
371+
{
372+
regex: /.*youtube.com\/embed\/([a-z0-9\?&=\-_]+).*/i,
373+
w: 560, h: 314,
374+
url: 'https://www.youtube.com/embed/$1',
375+
},
376+
];
377+
346378
const videoExtensions = ['mp4', 'mpeg', 'm4v', 'm4p', 'mov'];
347379
const audioExtensions = ['3gp', 'aac', 'flac', 'mp3', 'm4a', 'ogg', 'wav', 'webm'];
348380
const iframeExtensions = ['html', 'htm', 'php', 'asp', 'aspx', ''];
349381

350382
export function $createMediaNodeFromSrc(src: string): MediaNode {
383+
384+
for (const pattern of urlPatterns) {
385+
const match = src.match(pattern.regex);
386+
if (match) {
387+
const newSrc = src.replace(pattern.regex, pattern.url);
388+
const node = new MediaNode('iframe');
389+
node.setSrc(newSrc);
390+
node.setHeight(pattern.h);
391+
node.setWidth(pattern.w);
392+
return node;
393+
}
394+
}
395+
351396
let nodeTag: MediaNodeTag = 'iframe';
352397
const srcEnd = src.split('?')[0].split('/').pop() || '';
353398
const srcEndSplit = srcEnd.split('.');
@@ -360,7 +405,9 @@ export function $createMediaNodeFromSrc(src: string): MediaNode {
360405
nodeTag = 'embed';
361406
}
362407

363-
return new MediaNode(nodeTag);
408+
const node = new MediaNode(nodeTag);
409+
node.setSrc(src);
410+
return node;
364411
}
365412

366413
export function $isMediaNode(node: LexicalNode | null | undefined): node is MediaNode {

0 commit comments

Comments
 (0)