Merge pull request #5349 from BookStackApp/lexical_reorg

Lexical: Merge of custom nodes & re-organisation of codebase
This commit is contained in:
Dan Brown 2024-12-04 20:06:39 +00:00 committed by GitHub
commit 7e6f6af463
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
92 changed files with 1318 additions and 2893 deletions

View File

@ -1,4 +1,4 @@
import {$getSelection, createEditor, CreateEditorArgs, isCurrentlyReadOnlyMode, LexicalEditor} from 'lexical'; import {$getSelection, createEditor, CreateEditorArgs, LexicalEditor} from 'lexical';
import {createEmptyHistoryState, registerHistory} from '@lexical/history'; import {createEmptyHistoryState, registerHistory} from '@lexical/history';
import {registerRichText} from '@lexical/rich-text'; import {registerRichText} from '@lexical/rich-text';
import {mergeRegister} from '@lexical/utils'; import {mergeRegister} from '@lexical/utils';

View File

@ -355,7 +355,6 @@ function onSelectionChange(
lastNode instanceof ParagraphNode && lastNode instanceof ParagraphNode &&
lastNode.getChildrenSize() === 0 lastNode.getChildrenSize() === 0
) { ) {
selection.format = lastNode.getTextFormat();
selection.style = lastNode.getTextStyle(); selection.style = lastNode.getTextStyle();
} else { } else {
selection.format = 0; selection.format = 0;
@ -578,7 +577,6 @@ function onBeforeInput(event: InputEvent, editor: LexicalEditor): void {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode(); const anchorNode = selection.anchor.getNode();
anchorNode.markDirty(); anchorNode.markDirty();
selection.format = anchorNode.getFormat();
invariant( invariant(
$isTextNode(anchorNode), $isTextNode(anchorNode),
'Anchor node must be a TextNode', 'Anchor node must be a TextNode',
@ -912,7 +910,6 @@ function onCompositionStart(
// need to invoke the empty space heuristic below. // need to invoke the empty space heuristic below.
anchor.type === 'element' || anchor.type === 'element' ||
!selection.isCollapsed() || !selection.isCollapsed() ||
node.getFormat() !== selection.format ||
($isTextNode(node) && node.getStyle() !== selection.style) ($isTextNode(node) && node.getStyle() !== selection.style)
) { ) {
// We insert a zero width character, ready for the composition // We insert a zero width character, ready for the composition

View File

@ -16,7 +16,6 @@ import {
$getSelection, $getSelection,
$isDecoratorNode, $isDecoratorNode,
$isElementNode, $isElementNode,
$isRangeSelection,
$isTextNode, $isTextNode,
$setSelection, $setSelection,
} from '.'; } from '.';
@ -96,15 +95,6 @@ function shouldUpdateTextNodeFromMutation(
targetDOM: Node, targetDOM: Node,
targetNode: TextNode, targetNode: TextNode,
): boolean { ): boolean {
if ($isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode();
if (
anchorNode.is(targetNode) &&
selection.format !== anchorNode.getFormat()
) {
return false;
}
}
return targetDOM.nodeType === DOM_TEXT_TYPE && targetNode.isAttached(); return targetDOM.nodeType === DOM_TEXT_TYPE && targetNode.isAttached();
} }

View File

@ -17,7 +17,6 @@ import type {NodeKey, NodeMap} from './LexicalNode';
import type {ElementNode} from './nodes/LexicalElementNode'; import type {ElementNode} from './nodes/LexicalElementNode';
import invariant from 'lexical/shared/invariant'; import invariant from 'lexical/shared/invariant';
import normalizeClassNames from 'lexical/shared/normalizeClassNames';
import { import {
$isDecoratorNode, $isDecoratorNode,
@ -30,12 +29,12 @@ import {
import { import {
DOUBLE_LINE_BREAK, DOUBLE_LINE_BREAK,
FULL_RECONCILE, FULL_RECONCILE,
IS_ALIGN_CENTER,
IS_ALIGN_END,
IS_ALIGN_JUSTIFY,
IS_ALIGN_LEFT,
IS_ALIGN_RIGHT,
IS_ALIGN_START,
} from './LexicalConstants'; } from './LexicalConstants';
import {EditorState} from './LexicalEditorState'; import {EditorState} from './LexicalEditorState';
import { import {
@ -117,51 +116,6 @@ function setTextAlign(domStyle: CSSStyleDeclaration, value: string): void {
domStyle.setProperty('text-align', value); domStyle.setProperty('text-align', value);
} }
const DEFAULT_INDENT_VALUE = '40px';
function setElementIndent(dom: HTMLElement, indent: number): void {
const indentClassName = activeEditorConfig.theme.indent;
if (typeof indentClassName === 'string') {
const elementHasClassName = dom.classList.contains(indentClassName);
if (indent > 0 && !elementHasClassName) {
dom.classList.add(indentClassName);
} else if (indent < 1 && elementHasClassName) {
dom.classList.remove(indentClassName);
}
}
const indentationBaseValue =
getComputedStyle(dom).getPropertyValue('--lexical-indent-base-value') ||
DEFAULT_INDENT_VALUE;
dom.style.setProperty(
'padding-inline-start',
indent === 0 ? '' : `calc(${indent} * ${indentationBaseValue})`,
);
}
function setElementFormat(dom: HTMLElement, format: number): void {
const domStyle = dom.style;
if (format === 0) {
setTextAlign(domStyle, '');
} else if (format === IS_ALIGN_LEFT) {
setTextAlign(domStyle, 'left');
} else if (format === IS_ALIGN_CENTER) {
setTextAlign(domStyle, 'center');
} else if (format === IS_ALIGN_RIGHT) {
setTextAlign(domStyle, 'right');
} else if (format === IS_ALIGN_JUSTIFY) {
setTextAlign(domStyle, 'justify');
} else if (format === IS_ALIGN_START) {
setTextAlign(domStyle, 'start');
} else if (format === IS_ALIGN_END) {
setTextAlign(domStyle, 'end');
}
}
function $createNode( function $createNode(
key: NodeKey, key: NodeKey,
parentDOM: null | HTMLElement, parentDOM: null | HTMLElement,
@ -185,22 +139,14 @@ function $createNode(
} }
if ($isElementNode(node)) { if ($isElementNode(node)) {
const indent = node.__indent;
const childrenSize = node.__size; const childrenSize = node.__size;
if (indent !== 0) {
setElementIndent(dom, indent);
}
if (childrenSize !== 0) { if (childrenSize !== 0) {
const endIndex = childrenSize - 1; const endIndex = childrenSize - 1;
const children = createChildrenArray(node, activeNextNodeMap); const children = createChildrenArray(node, activeNextNodeMap);
$createChildren(children, node, 0, endIndex, dom, null); $createChildren(children, node, 0, endIndex, dom, null);
} }
const format = node.__format;
if (format !== 0) {
setElementFormat(dom, format);
}
if (!node.isInline()) { if (!node.isInline()) {
reconcileElementTerminatingLineBreak(null, node, dom); reconcileElementTerminatingLineBreak(null, node, dom);
} }
@ -349,10 +295,8 @@ function reconcileParagraphFormat(element: ElementNode): void {
if ( if (
$isParagraphNode(element) && $isParagraphNode(element) &&
subTreeTextFormat != null && subTreeTextFormat != null &&
subTreeTextFormat !== element.__textFormat &&
!activeEditorStateReadOnly !activeEditorStateReadOnly
) { ) {
element.setTextFormat(subTreeTextFormat);
element.setTextStyle(subTreeTextStyle); element.setTextStyle(subTreeTextStyle);
} }
} }
@ -563,17 +507,6 @@ function $reconcileNode(
if ($isElementNode(prevNode) && $isElementNode(nextNode)) { if ($isElementNode(prevNode) && $isElementNode(nextNode)) {
// Reconcile element children // Reconcile element children
const nextIndent = nextNode.__indent;
if (nextIndent !== prevNode.__indent) {
setElementIndent(dom, nextIndent);
}
const nextFormat = nextNode.__format;
if (nextFormat !== prevNode.__format) {
setElementFormat(dom, nextFormat);
}
if (isDirty) { if (isDirty) {
$reconcileChildrenWithDirection(prevNode, nextNode, dom); $reconcileChildrenWithDirection(prevNode, nextNode, dom);
if (!$isRootNode(nextNode) && !nextNode.isInline()) { if (!$isRootNode(nextNode) && !nextNode.isInline()) {

File diff suppressed because one or more lines are too long

View File

@ -47,7 +47,6 @@ import {
import invariant from 'lexical/shared/invariant'; import invariant from 'lexical/shared/invariant';
import { import {
$createTestDecoratorNode,
$createTestElementNode, $createTestElementNode,
$createTestInlineElementNode, $createTestInlineElementNode,
createTestEditor, createTestEditor,
@ -975,7 +974,7 @@ describe('LexicalEditor tests', () => {
editable ? 'editable' : 'non-editable' editable ? 'editable' : 'non-editable'
})`, async () => { })`, async () => {
const JSON_EDITOR_STATE = const JSON_EDITOR_STATE =
'{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"123","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'; '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"123","type":"text","version":1}],"direction":null,"type":"paragraph","version":1,"textStyle":""}],"direction":null,"type":"root","version":1}}';
init(); init();
const contentEditable = editor.getRootElement(); const contentEditable = editor.getRootElement();
editor.setEditable(editable); editor.setEditable(editable);
@ -1048,8 +1047,6 @@ describe('LexicalEditor tests', () => {
__cachedText: null, __cachedText: null,
__dir: null, __dir: null,
__first: paragraphKey, __first: paragraphKey,
__format: 0,
__indent: 0,
__key: 'root', __key: 'root',
__last: paragraphKey, __last: paragraphKey,
__next: null, __next: null,
@ -1060,10 +1057,11 @@ describe('LexicalEditor tests', () => {
__type: 'root', __type: 'root',
}); });
expect(parsedParagraph).toEqual({ expect(parsedParagraph).toEqual({
"__alignment": "",
__dir: null, __dir: null,
__first: textKey, __first: textKey,
__format: 0, __id: '',
__indent: 0, __inset: 0,
__key: paragraphKey, __key: paragraphKey,
__last: textKey, __last: textKey,
__next: null, __next: null,
@ -1071,7 +1069,6 @@ describe('LexicalEditor tests', () => {
__prev: null, __prev: null,
__size: 1, __size: 1,
__style: '', __style: '',
__textFormat: 0,
__textStyle: '', __textStyle: '',
__type: 'paragraph', __type: 'paragraph',
}); });
@ -1130,8 +1127,6 @@ describe('LexicalEditor tests', () => {
__cachedText: null, __cachedText: null,
__dir: null, __dir: null,
__first: paragraphKey, __first: paragraphKey,
__format: 0,
__indent: 0,
__key: 'root', __key: 'root',
__last: paragraphKey, __last: paragraphKey,
__next: null, __next: null,
@ -1142,10 +1137,11 @@ describe('LexicalEditor tests', () => {
__type: 'root', __type: 'root',
}); });
expect(parsedParagraph).toEqual({ expect(parsedParagraph).toEqual({
"__alignment": "",
__dir: null, __dir: null,
__first: textKey, __first: textKey,
__format: 0, __id: '',
__indent: 0, __inset: 0,
__key: paragraphKey, __key: paragraphKey,
__last: textKey, __last: textKey,
__next: null, __next: null,
@ -1153,7 +1149,6 @@ describe('LexicalEditor tests', () => {
__prev: null, __prev: null,
__size: 1, __size: 1,
__style: '', __style: '',
__textFormat: 0,
__textStyle: '', __textStyle: '',
__type: 'paragraph', __type: 'paragraph',
}); });

View File

@ -54,8 +54,6 @@ describe('LexicalEditorState tests', () => {
__cachedText: 'foo', __cachedText: 'foo',
__dir: null, __dir: null,
__first: '1', __first: '1',
__format: 0,
__indent: 0,
__key: 'root', __key: 'root',
__last: '1', __last: '1',
__next: null, __next: null,
@ -66,10 +64,11 @@ describe('LexicalEditorState tests', () => {
__type: 'root', __type: 'root',
}); });
expect(paragraph).toEqual({ expect(paragraph).toEqual({
"__alignment": "",
__dir: null, __dir: null,
__first: '2', __first: '2',
__format: 0, __id: '',
__indent: 0, __inset: 0,
__key: '1', __key: '1',
__last: '2', __last: '2',
__next: null, __next: null,
@ -77,7 +76,6 @@ describe('LexicalEditorState tests', () => {
__prev: null, __prev: null,
__size: 1, __size: 1,
__style: '', __style: '',
__textFormat: 0,
__textStyle: '', __textStyle: '',
__type: 'paragraph', __type: 'paragraph',
}); });
@ -113,7 +111,7 @@ describe('LexicalEditorState tests', () => {
}); });
expect(JSON.stringify(editor.getEditorState().toJSON())).toEqual( 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,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"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,"textStyle":""}],"direction":null,"type":"root","version":1}}`,
); );
}); });
@ -140,8 +138,6 @@ describe('LexicalEditorState tests', () => {
__cachedText: '', __cachedText: '',
__dir: null, __dir: null,
__first: null, __first: null,
__format: 0,
__indent: 0,
__key: 'root', __key: 'root',
__last: null, __last: null,
__next: null, __next: null,

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,6 @@ import {createHeadlessEditor} from '@lexical/headless';
import {AutoLinkNode, LinkNode} from '@lexical/link'; import {AutoLinkNode, LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list'; import {ListItemNode, ListNode} from '@lexical/list';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table'; import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
import { import {
@ -36,6 +35,8 @@ import {
LexicalNodeReplacement, LexicalNodeReplacement,
} from '../../LexicalEditor'; } from '../../LexicalEditor';
import {resetRandomKey} from '../../LexicalUtils'; import {resetRandomKey} from '../../LexicalUtils';
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
type TestEnv = { type TestEnv = {
@ -129,8 +130,6 @@ export class TestElementNode extends ElementNode {
serializedNode: SerializedTestElementNode, serializedNode: SerializedTestElementNode,
): TestInlineElementNode { ): TestInlineElementNode {
const node = $createTestInlineElementNode(); const node = $createTestInlineElementNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }
@ -195,8 +194,6 @@ export class TestInlineElementNode extends ElementNode {
serializedNode: SerializedTestInlineElementNode, serializedNode: SerializedTestInlineElementNode,
): TestInlineElementNode { ): TestInlineElementNode {
const node = $createTestInlineElementNode(); const node = $createTestInlineElementNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }
@ -241,8 +238,6 @@ export class TestShadowRootNode extends ElementNode {
serializedNode: SerializedTestShadowRootNode, serializedNode: SerializedTestShadowRootNode,
): TestShadowRootNode { ): TestShadowRootNode {
const node = $createTestShadowRootNode(); const node = $createTestShadowRootNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }
@ -322,8 +317,6 @@ export class TestExcludeFromCopyElementNode extends ElementNode {
serializedNode: SerializedTestExcludeFromCopyElementNode, serializedNode: SerializedTestExcludeFromCopyElementNode,
): TestExcludeFromCopyElementNode { ): TestExcludeFromCopyElementNode {
const node = $createTestExcludeFromCopyElementNode(); const node = $createTestExcludeFromCopyElementNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }

View File

@ -0,0 +1,61 @@
import {ElementNode, type SerializedElementNode} from "./LexicalElementNode";
import {CommonBlockAlignment, CommonBlockInterface} from "./common";
import {Spread} from "lexical";
export type SerializedCommonBlockNode = Spread<{
id: string;
alignment: CommonBlockAlignment;
inset: number;
}, SerializedElementNode>
export class CommonBlockNode extends ElementNode implements CommonBlockInterface {
__id: string = '';
__alignment: CommonBlockAlignment = '';
__inset: number = 0;
setId(id: string) {
const self = this.getWritable();
self.__id = id;
}
getId(): string {
const self = this.getLatest();
return self.__id;
}
setAlignment(alignment: CommonBlockAlignment) {
const self = this.getWritable();
self.__alignment = alignment;
}
getAlignment(): CommonBlockAlignment {
const self = this.getLatest();
return self.__alignment;
}
setInset(size: number) {
const self = this.getWritable();
self.__inset = size;
}
getInset(): number {
const self = this.getLatest();
return self.__inset;
}
exportJSON(): SerializedCommonBlockNode {
return {
...super.exportJSON(),
id: this.__id,
alignment: this.__alignment,
inset: this.__inset,
};
}
}
export function copyCommonBlockProperties(from: CommonBlockNode, to: CommonBlockNode): void {
// to.__id = from.__id;
to.__alignment = from.__alignment;
to.__inset = from.__inset;
}

View File

@ -19,8 +19,8 @@ import invariant from 'lexical/shared/invariant';
import {$isTextNode, TextNode} from '../index'; import {$isTextNode, TextNode} from '../index';
import { import {
DOUBLE_LINE_BREAK, DOUBLE_LINE_BREAK,
ELEMENT_FORMAT_TO_TYPE,
ELEMENT_TYPE_TO_FORMAT,
} from '../LexicalConstants'; } from '../LexicalConstants';
import {LexicalNode} from '../LexicalNode'; import {LexicalNode} from '../LexicalNode';
import { import {
@ -42,8 +42,6 @@ export type SerializedElementNode<
{ {
children: Array<T>; children: Array<T>;
direction: 'ltr' | 'rtl' | null; direction: 'ltr' | 'rtl' | null;
format: ElementFormatType;
indent: number;
}, },
SerializedLexicalNode SerializedLexicalNode
>; >;
@ -74,12 +72,8 @@ export class ElementNode extends LexicalNode {
/** @internal */ /** @internal */
__size: number; __size: number;
/** @internal */ /** @internal */
__format: number;
/** @internal */
__style: string; __style: string;
/** @internal */ /** @internal */
__indent: number;
/** @internal */
__dir: 'ltr' | 'rtl' | null; __dir: 'ltr' | 'rtl' | null;
constructor(key?: NodeKey) { constructor(key?: NodeKey) {
@ -87,9 +81,7 @@ export class ElementNode extends LexicalNode {
this.__first = null; this.__first = null;
this.__last = null; this.__last = null;
this.__size = 0; this.__size = 0;
this.__format = 0;
this.__style = ''; this.__style = '';
this.__indent = 0;
this.__dir = null; this.__dir = null;
} }
@ -98,28 +90,14 @@ export class ElementNode extends LexicalNode {
this.__first = prevNode.__first; this.__first = prevNode.__first;
this.__last = prevNode.__last; this.__last = prevNode.__last;
this.__size = prevNode.__size; this.__size = prevNode.__size;
this.__indent = prevNode.__indent;
this.__format = prevNode.__format;
this.__style = prevNode.__style; this.__style = prevNode.__style;
this.__dir = prevNode.__dir; this.__dir = prevNode.__dir;
} }
getFormat(): number {
const self = this.getLatest();
return self.__format;
}
getFormatType(): ElementFormatType {
const format = this.getFormat();
return ELEMENT_FORMAT_TO_TYPE[format] || '';
}
getStyle(): string { getStyle(): string {
const self = this.getLatest(); const self = this.getLatest();
return self.__style; return self.__style;
} }
getIndent(): number {
const self = this.getLatest();
return self.__indent;
}
getChildren<T extends LexicalNode>(): Array<T> { getChildren<T extends LexicalNode>(): Array<T> {
const children: Array<T> = []; const children: Array<T> = [];
let child: T | null = this.getFirstChild(); let child: T | null = this.getFirstChild();
@ -301,13 +279,6 @@ export class ElementNode extends LexicalNode {
const self = this.getLatest(); const self = this.getLatest();
return self.__dir; return self.__dir;
} }
hasFormat(type: ElementFormatType): boolean {
if (type !== '') {
const formatFlag = ELEMENT_TYPE_TO_FORMAT[type];
return (this.getFormat() & formatFlag) !== 0;
}
return false;
}
// Mutators // Mutators
@ -378,21 +349,11 @@ export class ElementNode extends LexicalNode {
self.__dir = direction; self.__dir = direction;
return self; return self;
} }
setFormat(type: ElementFormatType): this {
const self = this.getWritable();
self.__format = type !== '' ? ELEMENT_TYPE_TO_FORMAT[type] : 0;
return this;
}
setStyle(style: string): this { setStyle(style: string): this {
const self = this.getWritable(); const self = this.getWritable();
self.__style = style || ''; self.__style = style || '';
return this; return this;
} }
setIndent(indentLevel: number): this {
const self = this.getWritable();
self.__indent = indentLevel;
return this;
}
splice( splice(
start: number, start: number,
deleteCount: number, deleteCount: number,
@ -528,8 +489,6 @@ export class ElementNode extends LexicalNode {
return { return {
children: [], children: [],
direction: this.getDirection(), direction: this.getDirection(),
format: this.getFormatType(),
indent: this.getIndent(),
type: 'element', type: 'element',
version: 1, version: 1,
}; };

View File

@ -19,39 +19,36 @@ import type {
LexicalNode, LexicalNode,
NodeKey, NodeKey,
} from '../LexicalNode'; } from '../LexicalNode';
import type {
ElementFormatType,
SerializedElementNode,
} from './LexicalElementNode';
import type {RangeSelection} from 'lexical'; import type {RangeSelection} from 'lexical';
import {TEXT_TYPE_TO_FORMAT} from '../LexicalConstants';
import { import {
$applyNodeReplacement, $applyNodeReplacement,
getCachedClassNameArray, getCachedClassNameArray,
isHTMLElement, isHTMLElement,
} from '../LexicalUtils'; } from '../LexicalUtils';
import {ElementNode} from './LexicalElementNode'; import {$isTextNode} from './LexicalTextNode';
import {$isTextNode, TextFormatType} from './LexicalTextNode'; import {
commonPropertiesDifferent, deserializeCommonBlockNode,
setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps
} from "./common";
import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
export type SerializedParagraphNode = Spread< export type SerializedParagraphNode = Spread<
{ {
textFormat: number;
textStyle: string; textStyle: string;
}, },
SerializedElementNode SerializedCommonBlockNode
>; >;
/** @noInheritDoc */ /** @noInheritDoc */
export class ParagraphNode extends ElementNode { export class ParagraphNode extends CommonBlockNode {
['constructor']!: KlassConstructor<typeof ParagraphNode>; ['constructor']!: KlassConstructor<typeof ParagraphNode>;
/** @internal */ /** @internal */
__textFormat: number;
__textStyle: string; __textStyle: string;
constructor(key?: NodeKey) { constructor(key?: NodeKey) {
super(key); super(key);
this.__textFormat = 0;
this.__textStyle = ''; this.__textStyle = '';
} }
@ -59,22 +56,6 @@ export class ParagraphNode extends ElementNode {
return 'paragraph'; 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 { getTextStyle(): string {
const self = this.getLatest(); const self = this.getLatest();
return self.__textStyle; return self.__textStyle;
@ -92,8 +73,8 @@ export class ParagraphNode extends ElementNode {
afterCloneFrom(prevNode: this) { afterCloneFrom(prevNode: this) {
super.afterCloneFrom(prevNode); super.afterCloneFrom(prevNode);
this.__textFormat = prevNode.__textFormat;
this.__textStyle = prevNode.__textStyle; this.__textStyle = prevNode.__textStyle;
copyCommonBlockProperties(prevNode, this);
} }
// View // View
@ -105,6 +86,9 @@ export class ParagraphNode extends ElementNode {
const domClassList = dom.classList; const domClassList = dom.classList;
domClassList.add(...classNames); domClassList.add(...classNames);
} }
updateElementWithCommonBlockProps(dom, this);
return dom; return dom;
} }
updateDOM( updateDOM(
@ -112,7 +96,7 @@ export class ParagraphNode extends ElementNode {
dom: HTMLElement, dom: HTMLElement,
config: EditorConfig, config: EditorConfig,
): boolean { ): boolean {
return false; return commonPropertiesDifferent(prevNode, this);
} }
static importDOM(): DOMConversionMap | null { static importDOM(): DOMConversionMap | null {
@ -131,16 +115,6 @@ export class ParagraphNode extends ElementNode {
if (this.isEmpty()) { if (this.isEmpty()) {
element.append(document.createElement('br')); element.append(document.createElement('br'));
} }
const formatType = this.getFormatType();
element.style.textAlign = formatType;
const indent = this.getIndent();
if (indent > 0) {
// padding-inline-start is not widely supported in email HTML, but
// Lexical Reconciler uses padding-inline-start. Using text-indent instead.
element.style.textIndent = `${indent * 20}px`;
}
} }
return { return {
@ -150,16 +124,13 @@ export class ParagraphNode extends ElementNode {
static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode { static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode {
const node = $createParagraphNode(); const node = $createParagraphNode();
node.setFormat(serializedNode.format); deserializeCommonBlockNode(serializedNode, node);
node.setIndent(serializedNode.indent);
node.setTextFormat(serializedNode.textFormat);
return node; return node;
} }
exportJSON(): SerializedParagraphNode { exportJSON(): SerializedParagraphNode {
return { return {
...super.exportJSON(), ...super.exportJSON(),
textFormat: this.getTextFormat(),
textStyle: this.getTextStyle(), textStyle: this.getTextStyle(),
type: 'paragraph', type: 'paragraph',
version: 1, version: 1,
@ -173,11 +144,9 @@ export class ParagraphNode extends ElementNode {
restoreSelection: boolean, restoreSelection: boolean,
): ParagraphNode { ): ParagraphNode {
const newElement = $createParagraphNode(); const newElement = $createParagraphNode();
newElement.setTextFormat(rangeSelection.format);
newElement.setTextStyle(rangeSelection.style); newElement.setTextStyle(rangeSelection.style);
const direction = this.getDirection(); const direction = this.getDirection();
newElement.setDirection(direction); newElement.setDirection(direction);
newElement.setFormat(this.getFormatType());
newElement.setStyle(this.getTextStyle()); newElement.setStyle(this.getTextStyle());
this.insertAfter(newElement, restoreSelection); this.insertAfter(newElement, restoreSelection);
return newElement; return newElement;
@ -210,13 +179,7 @@ export class ParagraphNode extends ElementNode {
function $convertParagraphElement(element: HTMLElement): DOMConversionOutput { function $convertParagraphElement(element: HTMLElement): DOMConversionOutput {
const node = $createParagraphNode(); const node = $createParagraphNode();
if (element.style) { setCommonBlockPropsFromElement(element, node);
node.setFormat(element.style.textAlign as ElementFormatType);
const indent = parseInt(element.style.textIndent, 10) / 20;
if (indent > 0) {
node.setIndent(indent);
}
}
return {node}; return {node};
} }

View File

@ -99,8 +99,6 @@ export class RootNode extends ElementNode {
static importJSON(serializedNode: SerializedRootNode): RootNode { static importJSON(serializedNode: SerializedRootNode): RootNode {
// We don't create a root, and instead use the existing root. // We don't create a root, and instead use the existing root.
const node = $getRoot(); const node = $getRoot();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }
@ -109,8 +107,6 @@ export class RootNode extends ElementNode {
return { return {
children: [], children: [],
direction: this.getDirection(), direction: this.getDirection(),
format: this.getFormatType(),
indent: this.getIndent(),
type: 'root', type: 'root',
version: 1, version: 1,
}; };

View File

@ -84,8 +84,6 @@ describe('LexicalElementNode tests', () => {
expect(node.exportJSON()).toStrictEqual({ expect(node.exportJSON()).toStrictEqual({
children: [], children: [],
direction: null, direction: null,
format: '',
indent: 0,
type: 'test_block', type: 'test_block',
version: 1, version: 1,
}); });

View File

@ -48,11 +48,11 @@ describe('LexicalParagraphNode tests', () => {
// logic is in place in the corresponding importJSON method // logic is in place in the corresponding importJSON method
// to accomodate these changes. // to accomodate these changes.
expect(node.exportJSON()).toStrictEqual({ expect(node.exportJSON()).toStrictEqual({
alignment: '',
children: [], children: [],
direction: null, direction: null,
format: '', id: '',
indent: 0, inset: 0,
textFormat: 0,
textStyle: '', textStyle: '',
type: 'paragraph', type: 'paragraph',
version: 1, version: 1,
@ -127,6 +127,21 @@ describe('LexicalParagraphNode tests', () => {
}); });
}); });
test('id is supported', async () => {
const {editor} = testEnv;
let paragraphNode: ParagraphNode;
await editor.update(() => {
paragraphNode = new ParagraphNode();
paragraphNode.setId('testid')
$getRoot().append(paragraphNode);
});
expect(testEnv.innerHTML).toBe(
'<p id="testid"><br></p>',
);
});
test('$createParagraphNode()', async () => { test('$createParagraphNode()', async () => {
const {editor} = testEnv; const {editor} = testEnv;

View File

@ -77,8 +77,6 @@ describe('LexicalRootNode tests', () => {
expect(node.exportJSON()).toStrictEqual({ expect(node.exportJSON()).toStrictEqual({
children: [], children: [],
direction: null, direction: null,
format: '',
indent: 0,
type: 'root', type: 'root',
version: 1, version: 1,
}); });

View File

@ -10,21 +10,14 @@ import {
$insertDataTransferForPlainText, $insertDataTransferForPlainText,
$insertDataTransferForRichText, $insertDataTransferForRichText,
} from '@lexical/clipboard'; } from '@lexical/clipboard';
import {$createListItemNode, $createListNode} from '@lexical/list';
import {$createHeadingNode, registerRichText} from '@lexical/rich-text';
import { import {
$createParagraphNode, $createParagraphNode,
$createRangeSelection,
$createTabNode, $createTabNode,
$createTextNode,
$getRoot, $getRoot,
$getSelection, $getSelection,
$insertNodes, $insertNodes,
$isElementNode,
$isRangeSelection, $isRangeSelection,
$isTextNode,
$setSelection,
KEY_TAB_COMMAND,
} from 'lexical'; } from 'lexical';
import { import {

View File

@ -41,9 +41,7 @@ import {
$setCompositionKey, $setCompositionKey,
getEditorStateTextContent, getEditorStateTextContent,
} from '../../../LexicalUtils'; } from '../../../LexicalUtils';
import {Text} from "@codemirror/state";
import {$generateHtmlFromNodes} from "@lexical/html"; import {$generateHtmlFromNodes} from "@lexical/html";
import {formatBold} from "@lexical/selection/__tests__/utils";
const editorConfig = Object.freeze({ const editorConfig = Object.freeze({
namespace: '', namespace: '',

View File

@ -1,18 +1,11 @@
import {LexicalNode, Spread} from "lexical"; import {sizeToPixels} from "../../../utils/dom";
import type {SerializedElementNode} from "lexical/nodes/LexicalElementNode"; import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
import {el, sizeToPixels} from "../utils/dom";
export type CommonBlockAlignment = 'left' | 'right' | 'center' | 'justify' | ''; export type CommonBlockAlignment = 'left' | 'right' | 'center' | 'justify' | '';
const validAlignments: CommonBlockAlignment[] = ['left', 'right', 'center', 'justify']; const validAlignments: CommonBlockAlignment[] = ['left', 'right', 'center', 'justify'];
type EditorNodeDirection = 'ltr' | 'rtl' | null; type EditorNodeDirection = 'ltr' | 'rtl' | null;
export type SerializedCommonBlockNode = Spread<{
id: string;
alignment: CommonBlockAlignment;
inset: number;
}, SerializedElementNode>
export interface NodeHasAlignment { export interface NodeHasAlignment {
readonly __alignment: CommonBlockAlignment; readonly __alignment: CommonBlockAlignment;
setAlignment(alignment: CommonBlockAlignment): void; setAlignment(alignment: CommonBlockAlignment): void;
@ -37,7 +30,7 @@ export interface NodeHasDirection {
getDirection(): EditorNodeDirection; getDirection(): EditorNodeDirection;
} }
interface CommonBlockInterface extends NodeHasId, NodeHasAlignment, NodeHasInset, NodeHasDirection {} export interface CommonBlockInterface extends NodeHasId, NodeHasAlignment, NodeHasInset, NodeHasDirection {}
export function extractAlignmentFromElement(element: HTMLElement): CommonBlockAlignment { export function extractAlignmentFromElement(element: HTMLElement): CommonBlockAlignment {
const textAlignStyle: string = element.style.textAlign || ''; const textAlignStyle: string = element.style.textAlign || '';

View File

@ -206,7 +206,7 @@ describe('LexicalHeadlessEditor', () => {
cleanup(); cleanup();
expect(html).toBe( expect(html).toBe(
'<p>hello world</p>', '<p dir="ltr">hello world</p>',
); );
}); });
}); });

View File

@ -13,13 +13,14 @@ import {createHeadlessEditor} from '@lexical/headless';
import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html'; import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
import {LinkNode} from '@lexical/link'; import {LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list'; import {ListItemNode, ListNode} from '@lexical/list';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import { import {
$createParagraphNode, $createParagraphNode,
$createRangeSelection, $createRangeSelection,
$createTextNode, $createTextNode,
$getRoot, $getRoot,
} from 'lexical'; } from 'lexical';
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
describe('HTML', () => { describe('HTML', () => {
type Input = Array<{ type Input = Array<{
@ -175,7 +176,7 @@ describe('HTML', () => {
}); });
expect(html).toBe( expect(html).toBe(
'<p style="text-align: center;">Hello world!</p>', '<p class="align-center">Hello world!</p>',
); );
}); });
@ -205,7 +206,7 @@ describe('HTML', () => {
}); });
expect(html).toBe( expect(html).toBe(
'<p style="text-align: center;">Hello world!</p>', '<p class="align-center">Hello world!</p>',
); );
}); });
}); });

View File

@ -327,9 +327,6 @@ function wrapContinuousInlines(
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]; const node = nodes[i];
if ($isBlockElementNode(node)) { if ($isBlockElementNode(node)) {
if (textAlign && !node.getFormat()) {
node.setFormat(textAlign);
}
out.push(node); out.push(node);
} else { } else {
continuousInlines.push(node); continuousInlines.push(node);
@ -338,7 +335,6 @@ function wrapContinuousInlines(
(i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1])) (i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1]))
) { ) {
const wrapper = createWrapperFn(); const wrapper = createWrapperFn();
wrapper.setFormat(textAlign);
wrapper.append(...continuousInlines); wrapper.append(...continuousInlines);
out.push(wrapper); out.push(wrapper);
continuousInlines = []; continuousInlines = [];

View File

@ -162,8 +162,6 @@ export class LinkNode extends ElementNode {
target: serializedNode.target, target: serializedNode.target,
title: serializedNode.title, title: serializedNode.title,
}); });
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }
@ -402,8 +400,6 @@ export class AutoLinkNode extends LinkNode {
target: serializedNode.target, target: serializedNode.target,
title: serializedNode.title, title: serializedNode.title,
}); });
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }

View File

@ -13,7 +13,6 @@ import type {
DOMConversionOutput, DOMConversionOutput,
DOMExportOutput, DOMExportOutput,
EditorConfig, EditorConfig,
EditorThemeClasses,
LexicalNode, LexicalNode,
NodeKey, NodeKey,
ParagraphNode, ParagraphNode,
@ -22,10 +21,6 @@ import type {
Spread, Spread,
} from 'lexical'; } from 'lexical';
import {
addClassNamesToElement,
removeClassNamesFromElement,
} from '@lexical/utils';
import { import {
$applyNodeReplacement, $applyNodeReplacement,
$createParagraphNode, $createParagraphNode,
@ -36,11 +31,11 @@ import {
LexicalEditor, LexicalEditor,
} from 'lexical'; } from 'lexical';
import invariant from 'lexical/shared/invariant'; import invariant from 'lexical/shared/invariant';
import normalizeClassNames from 'lexical/shared/normalizeClassNames';
import {$createListNode, $isListNode} from './'; import {$createListNode, $isListNode} from './';
import {$handleIndent, $handleOutdent, mergeLists} from './formatList'; import {mergeLists} from './formatList';
import {isNestedListNode} from './utils'; import {isNestedListNode} from './utils';
import {el} from "../../utils/dom";
export type SerializedListItemNode = Spread< export type SerializedListItemNode = Spread<
{ {
@ -74,11 +69,17 @@ export class ListItemNode extends ElementNode {
createDOM(config: EditorConfig): HTMLElement { createDOM(config: EditorConfig): HTMLElement {
const element = document.createElement('li'); const element = document.createElement('li');
const parent = this.getParent(); const parent = this.getParent();
if ($isListNode(parent) && parent.getListType() === 'check') { if ($isListNode(parent) && parent.getListType() === 'check') {
updateListItemChecked(element, this, null, parent); updateListItemChecked(element, this);
} }
element.value = this.__value; element.value = this.__value;
$setListItemThemeClassNames(element, config.theme, this);
if ($hasNestedListWithoutLabel(this)) {
element.style.listStyle = 'none';
}
return element; return element;
} }
@ -89,11 +90,12 @@ export class ListItemNode extends ElementNode {
): boolean { ): boolean {
const parent = this.getParent(); const parent = this.getParent();
if ($isListNode(parent) && parent.getListType() === 'check') { if ($isListNode(parent) && parent.getListType() === 'check') {
updateListItemChecked(dom, this, prevNode, parent); updateListItemChecked(dom, this);
} }
dom.style.listStyle = $hasNestedListWithoutLabel(this) ? 'none' : '';
// @ts-expect-error - this is always HTMLListItemElement // @ts-expect-error - this is always HTMLListItemElement
dom.value = this.__value; dom.value = this.__value;
$setListItemThemeClassNames(dom, config.theme, this);
return false; return false;
} }
@ -126,14 +128,26 @@ export class ListItemNode extends ElementNode {
const node = $createListItemNode(); const node = $createListItemNode();
node.setChecked(serializedNode.checked); node.setChecked(serializedNode.checked);
node.setValue(serializedNode.value); node.setValue(serializedNode.value);
node.setFormat(serializedNode.format);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }
exportDOM(editor: LexicalEditor): DOMExportOutput { exportDOM(editor: LexicalEditor): DOMExportOutput {
const element = this.createDOM(editor._config); const element = this.createDOM(editor._config);
element.style.textAlign = this.getFormatType();
if (element.classList.contains('task-list-item')) {
const input = el('input', {
type: 'checkbox',
disabled: 'disabled',
});
if (element.hasAttribute('checked')) {
input.setAttribute('checked', 'checked');
element.removeAttribute('checked');
}
element.prepend(input);
}
return { return {
element, element,
}; };
@ -172,7 +186,6 @@ export class ListItemNode extends ElementNode {
if ($isListItemNode(replaceWithNode)) { if ($isListItemNode(replaceWithNode)) {
return super.replace(replaceWithNode); return super.replace(replaceWithNode);
} }
this.setIndent(0);
const list = this.getParentOrThrow(); const list = this.getParentOrThrow();
if (!$isListNode(list)) { if (!$isListNode(list)) {
return replaceWithNode; return replaceWithNode;
@ -351,41 +364,6 @@ export class ListItemNode extends ElementNode {
this.setChecked(!this.__checked); this.setChecked(!this.__checked);
} }
getIndent(): number {
// If we don't have a parent, we are likely serializing
const parent = this.getParent();
if (parent === null) {
return this.getLatest().__indent;
}
// ListItemNode should always have a ListNode for a parent.
let listNodeParent = parent.getParentOrThrow();
let indentLevel = 0;
while ($isListItemNode(listNodeParent)) {
listNodeParent = listNodeParent.getParentOrThrow().getParentOrThrow();
indentLevel++;
}
return indentLevel;
}
setIndent(indent: number): this {
invariant(typeof indent === 'number', 'Invalid indent value.');
indent = Math.floor(indent);
invariant(indent >= 0, 'Indent value must be non-negative.');
let currentIndent = this.getIndent();
while (currentIndent !== indent) {
if (currentIndent < indent) {
$handleIndent(this);
currentIndent++;
} else {
$handleOutdent(this);
currentIndent--;
}
}
return this;
}
/** @deprecated @internal */ /** @deprecated @internal */
canInsertAfter(node: LexicalNode): boolean { canInsertAfter(node: LexicalNode): boolean {
return $isListItemNode(node); return $isListItemNode(node);
@ -428,89 +406,33 @@ export class ListItemNode extends ElementNode {
} }
} }
function $setListItemThemeClassNames( function $hasNestedListWithoutLabel(node: ListItemNode): boolean {
dom: HTMLElement, const children = node.getChildren();
editorThemeClasses: EditorThemeClasses, let hasLabel = false;
node: ListItemNode, let hasNestedList = false;
): void {
const classesToAdd = [];
const classesToRemove = [];
const listTheme = editorThemeClasses.list;
const listItemClassName = listTheme ? listTheme.listitem : undefined;
let nestedListItemClassName;
if (listTheme && listTheme.nested) { for (const child of children) {
nestedListItemClassName = listTheme.nested.listitem; if ($isListNode(child)) {
} hasNestedList = true;
} else if (child.getTextContent().trim().length > 0) {
if (listItemClassName !== undefined) { hasLabel = true;
classesToAdd.push(...normalizeClassNames(listItemClassName));
}
if (listTheme) {
const parentNode = node.getParent();
const isCheckList =
$isListNode(parentNode) && parentNode.getListType() === 'check';
const checked = node.getChecked();
if (!isCheckList || checked) {
classesToRemove.push(listTheme.listitemUnchecked);
}
if (!isCheckList || !checked) {
classesToRemove.push(listTheme.listitemChecked);
}
if (isCheckList) {
classesToAdd.push(
checked ? listTheme.listitemChecked : listTheme.listitemUnchecked,
);
} }
} }
if (nestedListItemClassName !== undefined) { return hasNestedList && !hasLabel;
const nestedListItemClasses = normalizeClassNames(nestedListItemClassName);
if (node.getChildren().some((child) => $isListNode(child))) {
classesToAdd.push(...nestedListItemClasses);
} else {
classesToRemove.push(...nestedListItemClasses);
}
}
if (classesToRemove.length > 0) {
removeClassNamesFromElement(dom, ...classesToRemove);
}
if (classesToAdd.length > 0) {
addClassNamesToElement(dom, ...classesToAdd);
}
} }
function updateListItemChecked( function updateListItemChecked(
dom: HTMLElement, dom: HTMLElement,
listItemNode: ListItemNode, listItemNode: ListItemNode,
prevListItemNode: ListItemNode | null,
listNode: ListNode,
): void { ): void {
// Only add attributes for leaf list items // Only set task list attrs for leaf list items
if ($isListNode(listItemNode.getFirstChild())) { const shouldBeTaskItem = !$isListNode(listItemNode.getFirstChild());
dom.removeAttribute('role'); dom.classList.toggle('task-list-item', shouldBeTaskItem);
dom.removeAttribute('tabIndex'); if (listItemNode.__checked) {
dom.removeAttribute('aria-checked'); dom.setAttribute('checked', 'checked');
} else { } else {
dom.setAttribute('role', 'checkbox'); dom.removeAttribute('checked');
dom.setAttribute('tabIndex', '-1');
if (
!prevListItemNode ||
listItemNode.__checked !== prevListItemNode.__checked
) {
dom.setAttribute(
'aria-checked',
listItemNode.getChecked() ? 'true' : 'false',
);
}
} }
} }

View File

@ -36,9 +36,11 @@ import {
updateChildrenListItemValue, updateChildrenListItemValue,
} from './formatList'; } from './formatList';
import {$getListDepth, $wrapInListItem} from './utils'; import {$getListDepth, $wrapInListItem} from './utils';
import {extractDirectionFromElement} from "lexical/nodes/common";
export type SerializedListNode = Spread< export type SerializedListNode = Spread<
{ {
id: string;
listType: ListType; listType: ListType;
start: number; start: number;
tag: ListNodeTagType; tag: ListNodeTagType;
@ -58,15 +60,18 @@ export class ListNode extends ElementNode {
__start: number; __start: number;
/** @internal */ /** @internal */
__listType: ListType; __listType: ListType;
/** @internal */
__id: string = '';
static getType(): string { static getType(): string {
return 'list'; return 'list';
} }
static clone(node: ListNode): ListNode { static clone(node: ListNode): ListNode {
const listType = node.__listType || TAG_TO_LIST_TYPE[node.__tag]; const newNode = new ListNode(node.__listType, node.__start, node.__key);
newNode.__id = node.__id;
return new ListNode(listType, node.__start, node.__key); newNode.__dir = node.__dir;
return newNode;
} }
constructor(listType: ListType, start: number, key?: NodeKey) { constructor(listType: ListType, start: number, key?: NodeKey) {
@ -81,6 +86,16 @@ export class ListNode extends ElementNode {
return this.__tag; return this.__tag;
} }
setId(id: string) {
const self = this.getWritable();
self.__id = id;
}
getId(): string {
const self = this.getLatest();
return self.__id;
}
setListType(type: ListType): void { setListType(type: ListType): void {
const writable = this.getWritable(); const writable = this.getWritable();
writable.__listType = type; writable.__listType = type;
@ -108,6 +123,14 @@ export class ListNode extends ElementNode {
dom.__lexicalListType = this.__listType; dom.__lexicalListType = this.__listType;
$setListThemeClassNames(dom, config.theme, this); $setListThemeClassNames(dom, config.theme, this);
if (this.__id) {
dom.setAttribute('id', this.__id);
}
if (this.__dir) {
dom.setAttribute('dir', this.__dir);
}
return dom; return dom;
} }
@ -116,7 +139,11 @@ export class ListNode extends ElementNode {
dom: HTMLElement, dom: HTMLElement,
config: EditorConfig, config: EditorConfig,
): boolean { ): boolean {
if (prevNode.__tag !== this.__tag) { if (
prevNode.__tag !== this.__tag
|| prevNode.__dir !== this.__dir
|| prevNode.__id !== this.__id
) {
return true; return true;
} }
@ -148,8 +175,7 @@ export class ListNode extends ElementNode {
static importJSON(serializedNode: SerializedListNode): ListNode { static importJSON(serializedNode: SerializedListNode): ListNode {
const node = $createListNode(serializedNode.listType, serializedNode.start); const node = $createListNode(serializedNode.listType, serializedNode.start);
node.setFormat(serializedNode.format); node.setId(serializedNode.id);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }
@ -177,6 +203,7 @@ export class ListNode extends ElementNode {
tag: this.getTag(), tag: this.getTag(),
type: 'list', type: 'list',
version: 1, version: 1,
id: this.__id,
}; };
} }
@ -277,28 +304,21 @@ function $setListThemeClassNames(
} }
/* /*
* This function normalizes the children of a ListNode after the conversion from HTML, * This function is a custom normalization function to allow nested lists within list item elements.
* ensuring that they are all ListItemNodes and contain either a single nested ListNode * Original taken from https://github.com/facebook/lexical/blob/6e10210fd1e113ccfafdc999b1d896733c5c5bea/packages/lexical-list/src/LexicalListNode.ts#L284-L303
* or some other inline content. * With modifications made.
*/ */
function $normalizeChildren(nodes: Array<LexicalNode>): Array<ListItemNode> { function $normalizeChildren(nodes: Array<LexicalNode>): Array<ListItemNode> {
const normalizedListItems: Array<ListItemNode> = []; const normalizedListItems: Array<ListItemNode> = [];
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]; for (const node of nodes) {
if ($isListItemNode(node)) { if ($isListItemNode(node)) {
normalizedListItems.push(node); normalizedListItems.push(node);
const children = node.getChildren();
if (children.length > 1) {
children.forEach((child) => {
if ($isListNode(child)) {
normalizedListItems.push($wrapInListItem(child));
}
});
}
} else { } else {
normalizedListItems.push($wrapInListItem(node)); normalizedListItems.push($wrapInListItem(node));
} }
} }
return normalizedListItems; return normalizedListItems;
} }
@ -334,6 +354,14 @@ function $convertListNode(domNode: HTMLElement): DOMConversionOutput {
} }
} }
if (domNode.id && node) {
node.setId(domNode.id);
}
if (domNode.dir && node) {
node.setDirection(extractDirectionFromElement(domNode));
}
return { return {
after: $normalizeChildren, after: $normalizeChildren,
node, node,

View File

@ -62,7 +62,7 @@ describe('LexicalListItemNode tests', () => {
expectHtmlToBeEqual( expectHtmlToBeEqual(
listItemNode.createDOM(editorConfig).outerHTML, listItemNode.createDOM(editorConfig).outerHTML,
html` html`
<li value="1" class="my-listItem-item-class"></li> <li value="1"></li>
`, `,
); );
@ -90,7 +90,7 @@ describe('LexicalListItemNode tests', () => {
expectHtmlToBeEqual( expectHtmlToBeEqual(
domElement.outerHTML, domElement.outerHTML,
html` html`
<li value="1" class="my-listItem-item-class"></li> <li value="1"></li>
`, `,
); );
const newListItemNode = new ListItemNode(); const newListItemNode = new ListItemNode();
@ -106,7 +106,7 @@ describe('LexicalListItemNode tests', () => {
expectHtmlToBeEqual( expectHtmlToBeEqual(
domElement.outerHTML, domElement.outerHTML,
html` html`
<li value="1" class="my-listItem-item-class"></li> <li value="1"></li>
`, `,
); );
}); });
@ -125,7 +125,7 @@ describe('LexicalListItemNode tests', () => {
expectHtmlToBeEqual( expectHtmlToBeEqual(
domElement.outerHTML, domElement.outerHTML,
html` html`
<li value="1" class="my-listItem-item-class"></li> <li value="1"></li>
`, `,
); );
const nestedListNode = new ListNode('bullet', 1); const nestedListNode = new ListNode('bullet', 1);
@ -142,7 +142,7 @@ describe('LexicalListItemNode tests', () => {
expectHtmlToBeEqual( expectHtmlToBeEqual(
domElement.outerHTML, domElement.outerHTML,
html` html`
<li value="1" class="my-listItem-item-class my-nested-list-listItem-class"></li> <li value="1" style="list-style: none;"></li>
`, `,
); );
}); });
@ -486,14 +486,10 @@ describe('LexicalListItemNode tests', () => {
}); });
expectHtmlToBeEqual( expectHtmlToBeEqual(
testEnv.outerHTML, testEnv.innerHTML,
html` html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul> <ul>
<li value="1"> <li value="1" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A</span> <span data-lexical-text="true">A</span>
@ -507,21 +503,16 @@ describe('LexicalListItemNode tests', () => {
<span data-lexical-text="true">B</span> <span data-lexical-text="true">B</span>
</li> </li>
</ul> </ul>
</div>
`, `,
); );
await editor.update(() => x.remove()); await editor.update(() => x.remove());
expectHtmlToBeEqual( expectHtmlToBeEqual(
testEnv.outerHTML, testEnv.innerHTML,
html` html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul> <ul>
<li value="1"> <li value="1" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A</span> <span data-lexical-text="true">A</span>
@ -532,7 +523,6 @@ describe('LexicalListItemNode tests', () => {
<span data-lexical-text="true">B</span> <span data-lexical-text="true">B</span>
</li> </li>
</ul> </ul>
</div>
`, `,
); );
}); });
@ -566,12 +556,8 @@ describe('LexicalListItemNode tests', () => {
}); });
expectHtmlToBeEqual( expectHtmlToBeEqual(
testEnv.outerHTML, testEnv.innerHTML,
html` html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A</span> <span data-lexical-text="true">A</span>
@ -579,7 +565,7 @@ describe('LexicalListItemNode tests', () => {
<li value="2"> <li value="2">
<span data-lexical-text="true">x</span> <span data-lexical-text="true">x</span>
</li> </li>
<li value="3"> <li value="3" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">B</span> <span data-lexical-text="true">B</span>
@ -587,24 +573,19 @@ describe('LexicalListItemNode tests', () => {
</ul> </ul>
</li> </li>
</ul> </ul>
</div>
`, `,
); );
await editor.update(() => x.remove()); await editor.update(() => x.remove());
expectHtmlToBeEqual( expectHtmlToBeEqual(
testEnv.outerHTML, testEnv.innerHTML,
html` html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A</span> <span data-lexical-text="true">A</span>
</li> </li>
<li value="2"> <li value="2" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">B</span> <span data-lexical-text="true">B</span>
@ -612,7 +593,6 @@ describe('LexicalListItemNode tests', () => {
</ul> </ul>
</li> </li>
</ul> </ul>
</div>
`, `,
); );
}); });
@ -650,14 +630,10 @@ describe('LexicalListItemNode tests', () => {
}); });
expectHtmlToBeEqual( expectHtmlToBeEqual(
testEnv.outerHTML, testEnv.innerHTML,
html` html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul> <ul>
<li value="1"> <li value="1" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A</span> <span data-lexical-text="true">A</span>
@ -667,7 +643,7 @@ describe('LexicalListItemNode tests', () => {
<li value="1"> <li value="1">
<span data-lexical-text="true">x</span> <span data-lexical-text="true">x</span>
</li> </li>
<li value="2"> <li value="2" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">B</span> <span data-lexical-text="true">B</span>
@ -675,21 +651,16 @@ describe('LexicalListItemNode tests', () => {
</ul> </ul>
</li> </li>
</ul> </ul>
</div>
`, `,
); );
await editor.update(() => x.remove()); await editor.update(() => x.remove());
expectHtmlToBeEqual( expectHtmlToBeEqual(
testEnv.outerHTML, testEnv.innerHTML,
html` html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul> <ul>
<li value="1"> <li value="1" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A</span> <span data-lexical-text="true">A</span>
@ -700,7 +671,6 @@ describe('LexicalListItemNode tests', () => {
</ul> </ul>
</li> </li>
</ul> </ul>
</div>
`, `,
); );
}); });
@ -746,19 +716,15 @@ describe('LexicalListItemNode tests', () => {
}); });
expectHtmlToBeEqual( expectHtmlToBeEqual(
testEnv.outerHTML, testEnv.innerHTML,
html` html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul> <ul>
<li value="1"> <li value="1" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A1</span> <span data-lexical-text="true">A1</span>
</li> </li>
<li value="2"> <li value="2" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A2</span> <span data-lexical-text="true">A2</span>
@ -770,7 +736,7 @@ describe('LexicalListItemNode tests', () => {
<li value="1"> <li value="1">
<span data-lexical-text="true">x</span> <span data-lexical-text="true">x</span>
</li> </li>
<li value="2"> <li value="2" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">B</span> <span data-lexical-text="true">B</span>
@ -778,26 +744,21 @@ describe('LexicalListItemNode tests', () => {
</ul> </ul>
</li> </li>
</ul> </ul>
</div>
`, `,
); );
await editor.update(() => x.remove()); await editor.update(() => x.remove());
expectHtmlToBeEqual( expectHtmlToBeEqual(
testEnv.outerHTML, testEnv.innerHTML,
html` html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul> <ul>
<li value="1"> <li value="1" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A1</span> <span data-lexical-text="true">A1</span>
</li> </li>
<li value="2"> <li value="2" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A2</span> <span data-lexical-text="true">A2</span>
@ -810,7 +771,6 @@ describe('LexicalListItemNode tests', () => {
</ul> </ul>
</li> </li>
</ul> </ul>
</div>
`, `,
); );
}); });
@ -856,14 +816,10 @@ describe('LexicalListItemNode tests', () => {
}); });
expectHtmlToBeEqual( expectHtmlToBeEqual(
testEnv.outerHTML, testEnv.innerHTML,
html` html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul> <ul>
<li value="1"> <li value="1" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A</span> <span data-lexical-text="true">A</span>
@ -873,9 +829,9 @@ describe('LexicalListItemNode tests', () => {
<li value="1"> <li value="1">
<span data-lexical-text="true">x</span> <span data-lexical-text="true">x</span>
</li> </li>
<li value="2"> <li value="2" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">B1</span> <span data-lexical-text="true">B1</span>
@ -888,26 +844,21 @@ describe('LexicalListItemNode tests', () => {
</ul> </ul>
</li> </li>
</ul> </ul>
</div>
`, `,
); );
await editor.update(() => x.remove()); await editor.update(() => x.remove());
expectHtmlToBeEqual( expectHtmlToBeEqual(
testEnv.outerHTML, testEnv.innerHTML,
html` html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul> <ul>
<li value="1"> <li value="1" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A</span> <span data-lexical-text="true">A</span>
</li> </li>
<li value="2"> <li value="2" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">B1</span> <span data-lexical-text="true">B1</span>
@ -920,7 +871,6 @@ describe('LexicalListItemNode tests', () => {
</ul> </ul>
</li> </li>
</ul> </ul>
</div>
`, `,
); );
}); });
@ -974,19 +924,15 @@ describe('LexicalListItemNode tests', () => {
}); });
expectHtmlToBeEqual( expectHtmlToBeEqual(
testEnv.outerHTML, testEnv.innerHTML,
html` html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul> <ul>
<li value="1"> <li value="1" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A1</span> <span data-lexical-text="true">A1</span>
</li> </li>
<li value="2"> <li value="2" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A2</span> <span data-lexical-text="true">A2</span>
@ -998,9 +944,9 @@ describe('LexicalListItemNode tests', () => {
<li value="1"> <li value="1">
<span data-lexical-text="true">x</span> <span data-lexical-text="true">x</span>
</li> </li>
<li value="2"> <li value="2" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">B1</span> <span data-lexical-text="true">B1</span>
@ -1013,26 +959,21 @@ describe('LexicalListItemNode tests', () => {
</ul> </ul>
</li> </li>
</ul> </ul>
</div>
`, `,
); );
await editor.update(() => x.remove()); await editor.update(() => x.remove());
expectHtmlToBeEqual( expectHtmlToBeEqual(
testEnv.outerHTML, testEnv.innerHTML,
html` html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul> <ul>
<li value="1"> <li value="1" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A1</span> <span data-lexical-text="true">A1</span>
</li> </li>
<li value="2"> <li value="2" style="list-style: none;">
<ul> <ul>
<li value="1"> <li value="1">
<span data-lexical-text="true">A2</span> <span data-lexical-text="true">A2</span>
@ -1048,7 +989,6 @@ describe('LexicalListItemNode tests', () => {
</ul> </ul>
</li> </li>
</ul> </ul>
</div>
`, `,
); );
}); });
@ -1265,99 +1205,5 @@ describe('LexicalListItemNode tests', () => {
expect($isListItemNode(listItemNode)).toBe(true); expect($isListItemNode(listItemNode)).toBe(true);
}); });
}); });
describe('ListItemNode.setIndent()', () => {
let listNode: ListNode;
let listItemNode1: ListItemNode;
let listItemNode2: ListItemNode;
beforeEach(async () => {
const {editor} = testEnv;
await editor.update(() => {
const root = $getRoot();
listNode = new ListNode('bullet', 1);
listItemNode1 = new ListItemNode();
listItemNode2 = new ListItemNode();
root.append(listNode);
listNode.append(listItemNode1, listItemNode2);
listItemNode1.append(new TextNode('one'));
listItemNode2.append(new TextNode('two'));
});
});
it('indents and outdents list item', async () => {
const {editor} = testEnv;
await editor.update(() => {
listItemNode1.setIndent(3);
});
await editor.update(() => {
expect(listItemNode1.getIndent()).toBe(3);
});
expectHtmlToBeEqual(
editor.getRootElement()!.innerHTML,
html`
<ul>
<li value="1">
<ul>
<li value="1">
<ul>
<li value="1">
<ul>
<li value="1">
<span data-lexical-text="true">one</span>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">two</span>
</li>
</ul>
`,
);
await editor.update(() => {
listItemNode1.setIndent(0);
});
await editor.update(() => {
expect(listItemNode1.getIndent()).toBe(0);
});
expectHtmlToBeEqual(
editor.getRootElement()!.innerHTML,
html`
<ul>
<li value="1">
<span data-lexical-text="true">one</span>
</li>
<li value="2">
<span data-lexical-text="true">two</span>
</li>
</ul>
`,
);
});
it('handles fractional indent values', async () => {
const {editor} = testEnv;
await editor.update(() => {
listItemNode1.setIndent(0.5);
});
await editor.update(() => {
expect(listItemNode1.getIndent()).toBe(0);
});
});
});
}); });
}); });

View File

@ -294,24 +294,5 @@ describe('LexicalListNode tests', () => {
expect(bulletList.__listType).toBe('bullet'); expect(bulletList.__listType).toBe('bullet');
}); });
}); });
test('ListNode.clone() without list type (backward compatibility)', async () => {
const {editor} = testEnv;
await editor.update(() => {
const olNode = ListNode.clone({
__key: '1',
__start: 1,
__tag: 'ol',
} as unknown as ListNode);
const ulNode = ListNode.clone({
__key: '1',
__start: 1,
__tag: 'ul',
} as unknown as ListNode);
expect(olNode.__listType).toBe('number');
expect(ulNode.__listType).toBe('bullet');
});
});
}); });
}); });

View File

@ -84,10 +84,6 @@ export function insertList(editor: LexicalEditor, listType: ListType): void {
if ($isRootOrShadowRoot(anchorNodeParent)) { if ($isRootOrShadowRoot(anchorNodeParent)) {
anchorNode.replace(list); anchorNode.replace(list);
const listItem = $createListItemNode(); const listItem = $createListItemNode();
if ($isElementNode(anchorNode)) {
listItem.setFormat(anchorNode.getFormatType());
listItem.setIndent(anchorNode.getIndent());
}
list.append(listItem); list.append(listItem);
} else if ($isListItemNode(anchorNode)) { } else if ($isListItemNode(anchorNode)) {
const parent = anchorNode.getParentOrThrow(); const parent = anchorNode.getParentOrThrow();
@ -157,8 +153,6 @@ function $createListOrMerge(node: ElementNode, listType: ListType): ListNode {
const previousSibling = node.getPreviousSibling(); const previousSibling = node.getPreviousSibling();
const nextSibling = node.getNextSibling(); const nextSibling = node.getNextSibling();
const listItem = $createListItemNode(); const listItem = $createListItemNode();
listItem.setFormat(node.getFormatType());
listItem.setIndent(node.getIndent());
append(listItem, node.getChildren()); append(listItem, node.getChildren());
if ( if (

View File

@ -9,4 +9,4 @@ Only components used, or intended to be used, were copied in at this point.
The original work built upon in this directory and below is under the copyright of Meta Platforms, Inc. and affiliates. The original work built upon in this directory and below is under the copyright of Meta Platforms, Inc. and affiliates.
The original license can be seen in the [ORIGINAL-LEXICAL-LICENSE](./ORIGINAL-LEXICAL-LICENSE) file. The original license can be seen in the [ORIGINAL-LEXICAL-LICENSE](./ORIGINAL-LEXICAL-LICENSE) file.
Files may have since been modified with modifications being under the license and copyright of the BookStack project as a whole. Files may have since been added or modified with changes being under the license and copyright of the BookStack project as a whole.

View File

@ -11,10 +11,10 @@ import type {EditorConfig} from "lexical/LexicalEditor";
import type {RangeSelection} from "lexical/LexicalSelection"; import type {RangeSelection} from "lexical/LexicalSelection";
import { import {
CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode, CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
SerializedCommonBlockNode,
setCommonBlockPropsFromElement, setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps updateElementWithCommonBlockProps
} from "./_common"; } from "lexical/nodes/common";
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
export type CalloutCategory = 'info' | 'danger' | 'warning' | 'success'; export type CalloutCategory = 'info' | 'danger' | 'warning' | 'success';

View File

@ -8,9 +8,9 @@ import {
Spread Spread
} from "lexical"; } from "lexical";
import type {EditorConfig} from "lexical/LexicalEditor"; import type {EditorConfig} from "lexical/LexicalEditor";
import {EditorDecoratorAdapter} from "../ui/framework/decorator"; import {EditorDecoratorAdapter} from "../../ui/framework/decorator";
import {CodeEditor} from "../../components"; import {CodeEditor} from "../../../components";
import {el} from "../utils/dom"; import {el} from "../../utils/dom";
export type SerializedCodeBlockNode = Spread<{ export type SerializedCodeBlockNode = Spread<{
language: string; language: string;

View File

@ -8,8 +8,8 @@ import {
EditorConfig, EditorConfig,
} from 'lexical'; } from 'lexical';
import {el} from "../utils/dom"; import {el} from "../../utils/dom";
import {extractDirectionFromElement} from "./_common"; import {extractDirectionFromElement} from "lexical/nodes/common";
export type SerializedDetailsNode = Spread<{ export type SerializedDetailsNode = Spread<{
id: string; id: string;

View File

@ -8,8 +8,8 @@ import {
Spread Spread
} from "lexical"; } from "lexical";
import type {EditorConfig} from "lexical/LexicalEditor"; import type {EditorConfig} from "lexical/LexicalEditor";
import {EditorDecoratorAdapter} from "../ui/framework/decorator"; import {EditorDecoratorAdapter} from "../../ui/framework/decorator";
import {el} from "../utils/dom"; import {el} from "../../utils/dom";
export type SerializedDiagramNode = Spread<{ export type SerializedDiagramNode = Spread<{
id: string; id: string;

View File

@ -0,0 +1,201 @@
import {
$applyNodeReplacement,
$createParagraphNode,
type DOMConversionMap,
DOMConversionOutput,
type DOMExportOutput,
type EditorConfig,
isHTMLElement,
type LexicalEditor,
type LexicalNode,
type NodeKey,
type ParagraphNode,
type RangeSelection,
type Spread
} from "lexical";
import {addClassNamesToElement} from "@lexical/utils";
import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
import {
commonPropertiesDifferent, deserializeCommonBlockNode,
setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps
} from "lexical/nodes/common";
export type HeadingTagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export type SerializedHeadingNode = Spread<
{
tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
},
SerializedCommonBlockNode
>;
/** @noInheritDoc */
export class HeadingNode extends CommonBlockNode {
/** @internal */
__tag: HeadingTagType;
static getType(): string {
return 'heading';
}
static clone(node: HeadingNode): HeadingNode {
const clone = new HeadingNode(node.__tag, node.__key);
copyCommonBlockProperties(node, clone);
return clone;
}
constructor(tag: HeadingTagType, key?: NodeKey) {
super(key);
this.__tag = tag;
}
getTag(): HeadingTagType {
return this.__tag;
}
// View
createDOM(config: EditorConfig): HTMLElement {
const tag = this.__tag;
const element = document.createElement(tag);
const theme = config.theme;
const classNames = theme.heading;
if (classNames !== undefined) {
const className = classNames[tag];
addClassNamesToElement(element, className);
}
updateElementWithCommonBlockProps(element, this);
return element;
}
updateDOM(prevNode: HeadingNode, dom: HTMLElement): boolean {
return commonPropertiesDifferent(prevNode, this);
}
static importDOM(): DOMConversionMap | null {
return {
h1: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h2: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h3: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h4: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h5: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h6: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
};
}
exportDOM(editor: LexicalEditor): DOMExportOutput {
const {element} = super.exportDOM(editor);
if (element && isHTMLElement(element)) {
if (this.isEmpty()) {
element.append(document.createElement('br'));
}
}
return {
element,
};
}
static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
const node = $createHeadingNode(serializedNode.tag);
deserializeCommonBlockNode(serializedNode, node);
return node;
}
exportJSON(): SerializedHeadingNode {
return {
...super.exportJSON(),
tag: this.getTag(),
type: 'heading',
version: 1,
};
}
// Mutation
insertNewAfter(
selection?: RangeSelection,
restoreSelection = true,
): ParagraphNode | HeadingNode {
const anchorOffet = selection ? selection.anchor.offset : 0;
const lastDesc = this.getLastDescendant();
const isAtEnd =
!lastDesc ||
(selection &&
selection.anchor.key === lastDesc.getKey() &&
anchorOffet === lastDesc.getTextContentSize());
const newElement =
isAtEnd || !selection
? $createParagraphNode()
: $createHeadingNode(this.getTag());
const direction = this.getDirection();
newElement.setDirection(direction);
this.insertAfter(newElement, restoreSelection);
if (anchorOffet === 0 && !this.isEmpty() && selection) {
const paragraph = $createParagraphNode();
paragraph.select();
this.replace(paragraph, true);
}
return newElement;
}
collapseAtStart(): true {
const newElement = !this.isEmpty()
? $createHeadingNode(this.getTag())
: $createParagraphNode();
const children = this.getChildren();
children.forEach((child) => newElement.append(child));
this.replace(newElement);
return true;
}
extractWithChild(): boolean {
return true;
}
}
function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
const nodeName = element.nodeName.toLowerCase();
let node = null;
if (
nodeName === 'h1' ||
nodeName === 'h2' ||
nodeName === 'h3' ||
nodeName === 'h4' ||
nodeName === 'h5' ||
nodeName === 'h6'
) {
node = $createHeadingNode(nodeName);
setCommonBlockPropsFromElement(element, node);
}
return {node};
}
export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode {
return $applyNodeReplacement(new HeadingNode(headingTag));
}
export function $isHeadingNode(
node: LexicalNode | null | undefined,
): node is HeadingNode {
return node instanceof HeadingNode;
}

View File

@ -6,8 +6,8 @@ import {
Spread Spread
} from "lexical"; } from "lexical";
import type {EditorConfig} from "lexical/LexicalEditor"; import type {EditorConfig} from "lexical/LexicalEditor";
import {CommonBlockAlignment, extractAlignmentFromElement} from "./_common"; import {CommonBlockAlignment, extractAlignmentFromElement} from "lexical/nodes/common";
import {$selectSingleNode} from "../utils/selection"; import {$selectSingleNode} from "../../utils/selection";
import {SerializedElementNode} from "lexical/nodes/LexicalElementNode"; import {SerializedElementNode} from "lexical/nodes/LexicalElementNode";
export interface ImageNodeOptions { export interface ImageNodeOptions {

View File

@ -8,14 +8,14 @@ import {
} from 'lexical'; } from 'lexical';
import type {EditorConfig} from "lexical/LexicalEditor"; import type {EditorConfig} from "lexical/LexicalEditor";
import {el, setOrRemoveAttribute, sizeToPixels} from "../utils/dom"; import {el, setOrRemoveAttribute, sizeToPixels} from "../../utils/dom";
import { import {
CommonBlockAlignment, deserializeCommonBlockNode, CommonBlockAlignment, deserializeCommonBlockNode,
SerializedCommonBlockNode,
setCommonBlockPropsFromElement, setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps updateElementWithCommonBlockProps
} from "./_common"; } from "lexical/nodes/common";
import {$selectSingleNode} from "../utils/selection"; import {$selectSingleNode} from "../../utils/selection";
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio'; export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';
export type MediaNodeSource = { export type MediaNodeSource = {

View File

@ -0,0 +1,127 @@
import {
$applyNodeReplacement,
$createParagraphNode,
type DOMConversionMap,
type DOMConversionOutput,
type DOMExportOutput,
type EditorConfig,
isHTMLElement,
type LexicalEditor,
LexicalNode,
type NodeKey,
type ParagraphNode,
type RangeSelection
} from "lexical";
import {addClassNamesToElement} from "@lexical/utils";
import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
import {
commonPropertiesDifferent, deserializeCommonBlockNode,
setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps
} from "lexical/nodes/common";
export type SerializedQuoteNode = SerializedCommonBlockNode;
/** @noInheritDoc */
export class QuoteNode extends CommonBlockNode {
static getType(): string {
return 'quote';
}
static clone(node: QuoteNode): QuoteNode {
const clone = new QuoteNode(node.__key);
copyCommonBlockProperties(node, clone);
return clone;
}
constructor(key?: NodeKey) {
super(key);
}
// View
createDOM(config: EditorConfig): HTMLElement {
const element = document.createElement('blockquote');
addClassNamesToElement(element, config.theme.quote);
updateElementWithCommonBlockProps(element, this);
return element;
}
updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean {
return commonPropertiesDifferent(prevNode, this);
}
static importDOM(): DOMConversionMap | null {
return {
blockquote: (node: Node) => ({
conversion: $convertBlockquoteElement,
priority: 0,
}),
};
}
exportDOM(editor: LexicalEditor): DOMExportOutput {
const {element} = super.exportDOM(editor);
if (element && isHTMLElement(element)) {
if (this.isEmpty()) {
element.append(document.createElement('br'));
}
}
return {
element,
};
}
static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {
const node = $createQuoteNode();
deserializeCommonBlockNode(serializedNode, node);
return node;
}
exportJSON(): SerializedQuoteNode {
return {
...super.exportJSON(),
type: 'quote',
};
}
// Mutation
insertNewAfter(_: RangeSelection, restoreSelection?: boolean): ParagraphNode {
const newBlock = $createParagraphNode();
const direction = this.getDirection();
newBlock.setDirection(direction);
this.insertAfter(newBlock, restoreSelection);
return newBlock;
}
collapseAtStart(): true {
const paragraph = $createParagraphNode();
const children = this.getChildren();
children.forEach((child) => paragraph.append(child));
this.replace(paragraph);
return true;
}
canMergeWhenEmpty(): true {
return true;
}
}
export function $createQuoteNode(): QuoteNode {
return $applyNodeReplacement(new QuoteNode());
}
export function $isQuoteNode(
node: LexicalNode | null | undefined,
): node is QuoteNode {
return node instanceof QuoteNode;
}
function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
const node = $createQuoteNode();
setCommonBlockPropsFromElement(element, node);
return {node};
}

View File

@ -6,11 +6,6 @@
* *
*/ */
import {
$createHeadingNode,
$isHeadingNode,
HeadingNode,
} from '@lexical/rich-text';
import { import {
$createTextNode, $createTextNode,
$getRoot, $getRoot,
@ -19,6 +14,7 @@ import {
RangeSelection, RangeSelection,
} from 'lexical'; } from 'lexical';
import {initializeUnitTest} from 'lexical/__tests__/utils'; import {initializeUnitTest} from 'lexical/__tests__/utils';
import {$createHeadingNode, $isHeadingNode, HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
const editorConfig = Object.freeze({ const editorConfig = Object.freeze({
namespace: '', namespace: '',

View File

@ -6,9 +6,9 @@
* *
*/ */
import {$createQuoteNode, QuoteNode} from '@lexical/rich-text';
import {$createRangeSelection, $getRoot, ParagraphNode} from 'lexical'; import {$createRangeSelection, $getRoot, ParagraphNode} from 'lexical';
import {initializeUnitTest} from 'lexical/__tests__/utils'; import {initializeUnitTest} from 'lexical/__tests__/utils';
import {$createQuoteNode, QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
const editorConfig = Object.freeze({ const editorConfig = Object.freeze({
namespace: '', namespace: '',

View File

@ -8,42 +8,14 @@
import type { import type {
CommandPayloadType, CommandPayloadType,
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
EditorConfig,
ElementFormatType, ElementFormatType,
LexicalCommand, LexicalCommand,
LexicalEditor, LexicalEditor,
LexicalNode,
NodeKey,
ParagraphNode,
PasteCommandType, PasteCommandType,
RangeSelection, RangeSelection,
SerializedElementNode,
Spread,
TextFormatType, TextFormatType,
} from 'lexical'; } from 'lexical';
import { import {
$insertDataTransferForRichText,
copyToClipboard,
} from '@lexical/clipboard';
import {
$moveCharacter,
$shouldOverrideDefaultCharacterSelection,
} from '@lexical/selection';
import {
$findMatchingParent,
$getNearestBlockElementAncestorOrThrow,
addClassNamesToElement,
isHTMLElement,
mergeRegister,
objectKlassEquals,
} from '@lexical/utils';
import {
$applyNodeReplacement,
$createParagraphNode,
$createRangeSelection, $createRangeSelection,
$createTabNode, $createTabNode,
$getAdjacentNode, $getAdjacentNode,
@ -55,7 +27,6 @@ import {
$isElementNode, $isElementNode,
$isNodeSelection, $isNodeSelection,
$isRangeSelection, $isRangeSelection,
$isRootNode,
$isTextNode, $isTextNode,
$normalizeSelection__EXPERIMENTAL, $normalizeSelection__EXPERIMENTAL,
$selectAll, $selectAll,
@ -75,7 +46,6 @@ import {
ElementNode, ElementNode,
FORMAT_ELEMENT_COMMAND, FORMAT_ELEMENT_COMMAND,
FORMAT_TEXT_COMMAND, FORMAT_TEXT_COMMAND,
INDENT_CONTENT_COMMAND,
INSERT_LINE_BREAK_COMMAND, INSERT_LINE_BREAK_COMMAND,
INSERT_PARAGRAPH_COMMAND, INSERT_PARAGRAPH_COMMAND,
INSERT_TAB_COMMAND, INSERT_TAB_COMMAND,
@ -88,344 +58,22 @@ import {
KEY_DELETE_COMMAND, KEY_DELETE_COMMAND,
KEY_ENTER_COMMAND, KEY_ENTER_COMMAND,
KEY_ESCAPE_COMMAND, KEY_ESCAPE_COMMAND,
OUTDENT_CONTENT_COMMAND,
PASTE_COMMAND, PASTE_COMMAND,
REMOVE_TEXT_COMMAND, REMOVE_TEXT_COMMAND,
SELECT_ALL_COMMAND, SELECT_ALL_COMMAND,
} from 'lexical'; } from 'lexical';
import caretFromPoint from 'lexical/shared/caretFromPoint';
import {
CAN_USE_BEFORE_INPUT,
IS_APPLE_WEBKIT,
IS_IOS,
IS_SAFARI,
} from 'lexical/shared/environment';
export type SerializedHeadingNode = Spread< import {$insertDataTransferForRichText, copyToClipboard,} from '@lexical/clipboard';
{ import {$moveCharacter, $shouldOverrideDefaultCharacterSelection,} from '@lexical/selection';
tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; import {$findMatchingParent, mergeRegister, objectKlassEquals,} from '@lexical/utils';
}, import caretFromPoint from 'lexical/shared/caretFromPoint';
SerializedElementNode import {CAN_USE_BEFORE_INPUT, IS_APPLE_WEBKIT, IS_IOS, IS_SAFARI,} from 'lexical/shared/environment';
>;
export const DRAG_DROP_PASTE: LexicalCommand<Array<File>> = createCommand( export const DRAG_DROP_PASTE: LexicalCommand<Array<File>> = createCommand(
'DRAG_DROP_PASTE_FILE', 'DRAG_DROP_PASTE_FILE',
); );
export type SerializedQuoteNode = SerializedElementNode;
/** @noInheritDoc */
export class QuoteNode extends ElementNode {
static getType(): string {
return 'quote';
}
static clone(node: QuoteNode): QuoteNode {
return new QuoteNode(node.__key);
}
constructor(key?: NodeKey) {
super(key);
}
// View
createDOM(config: EditorConfig): HTMLElement {
const element = document.createElement('blockquote');
addClassNamesToElement(element, config.theme.quote);
return element;
}
updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean {
return false;
}
static importDOM(): DOMConversionMap | null {
return {
blockquote: (node: Node) => ({
conversion: $convertBlockquoteElement,
priority: 0,
}),
};
}
exportDOM(editor: LexicalEditor): DOMExportOutput {
const {element} = super.exportDOM(editor);
if (element && isHTMLElement(element)) {
if (this.isEmpty()) {
element.append(document.createElement('br'));
}
const formatType = this.getFormatType();
element.style.textAlign = formatType;
}
return {
element,
};
}
static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {
const node = $createQuoteNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
return node;
}
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
type: 'quote',
};
}
// Mutation
insertNewAfter(_: RangeSelection, restoreSelection?: boolean): ParagraphNode {
const newBlock = $createParagraphNode();
const direction = this.getDirection();
newBlock.setDirection(direction);
this.insertAfter(newBlock, restoreSelection);
return newBlock;
}
collapseAtStart(): true {
const paragraph = $createParagraphNode();
const children = this.getChildren();
children.forEach((child) => paragraph.append(child));
this.replace(paragraph);
return true;
}
canMergeWhenEmpty(): true {
return true;
}
}
export function $createQuoteNode(): QuoteNode {
return $applyNodeReplacement(new QuoteNode());
}
export function $isQuoteNode(
node: LexicalNode | null | undefined,
): node is QuoteNode {
return node instanceof QuoteNode;
}
export type HeadingTagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
/** @noInheritDoc */
export class HeadingNode extends ElementNode {
/** @internal */
__tag: HeadingTagType;
static getType(): string {
return 'heading';
}
static clone(node: HeadingNode): HeadingNode {
return new HeadingNode(node.__tag, node.__key);
}
constructor(tag: HeadingTagType, key?: NodeKey) {
super(key);
this.__tag = tag;
}
getTag(): HeadingTagType {
return this.__tag;
}
// View
createDOM(config: EditorConfig): HTMLElement {
const tag = this.__tag;
const element = document.createElement(tag);
const theme = config.theme;
const classNames = theme.heading;
if (classNames !== undefined) {
const className = classNames[tag];
addClassNamesToElement(element, className);
}
return element;
}
updateDOM(prevNode: HeadingNode, dom: HTMLElement): boolean {
return false;
}
static importDOM(): DOMConversionMap | null {
return {
h1: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h2: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h3: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h4: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h5: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h6: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
p: (node: Node) => {
// domNode is a <p> since we matched it by nodeName
const paragraph = node as HTMLParagraphElement;
const firstChild = paragraph.firstChild;
if (firstChild !== null && isGoogleDocsTitle(firstChild)) {
return {
conversion: () => ({node: null}),
priority: 3,
};
}
return null;
},
span: (node: Node) => {
if (isGoogleDocsTitle(node)) {
return {
conversion: (domNode: Node) => {
return {
node: $createHeadingNode('h1'),
};
},
priority: 3,
};
}
return null;
},
};
}
exportDOM(editor: LexicalEditor): DOMExportOutput {
const {element} = super.exportDOM(editor);
if (element && isHTMLElement(element)) {
if (this.isEmpty()) {
element.append(document.createElement('br'));
}
const formatType = this.getFormatType();
element.style.textAlign = formatType;
}
return {
element,
};
}
static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
const node = $createHeadingNode(serializedNode.tag);
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
return node;
}
exportJSON(): SerializedHeadingNode {
return {
...super.exportJSON(),
tag: this.getTag(),
type: 'heading',
version: 1,
};
}
// Mutation
insertNewAfter(
selection?: RangeSelection,
restoreSelection = true,
): ParagraphNode | HeadingNode {
const anchorOffet = selection ? selection.anchor.offset : 0;
const lastDesc = this.getLastDescendant();
const isAtEnd =
!lastDesc ||
(selection &&
selection.anchor.key === lastDesc.getKey() &&
anchorOffet === lastDesc.getTextContentSize());
const newElement =
isAtEnd || !selection
? $createParagraphNode()
: $createHeadingNode(this.getTag());
const direction = this.getDirection();
newElement.setDirection(direction);
this.insertAfter(newElement, restoreSelection);
if (anchorOffet === 0 && !this.isEmpty() && selection) {
const paragraph = $createParagraphNode();
paragraph.select();
this.replace(paragraph, true);
}
return newElement;
}
collapseAtStart(): true {
const newElement = !this.isEmpty()
? $createHeadingNode(this.getTag())
: $createParagraphNode();
const children = this.getChildren();
children.forEach((child) => newElement.append(child));
this.replace(newElement);
return true;
}
extractWithChild(): boolean {
return true;
}
}
function isGoogleDocsTitle(domNode: Node): boolean {
if (domNode.nodeName.toLowerCase() === 'span') {
return (domNode as HTMLSpanElement).style.fontSize === '26pt';
}
return false;
}
function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
const nodeName = element.nodeName.toLowerCase();
let node = null;
if (
nodeName === 'h1' ||
nodeName === 'h2' ||
nodeName === 'h3' ||
nodeName === 'h4' ||
nodeName === 'h5' ||
nodeName === 'h6'
) {
node = $createHeadingNode(nodeName);
if (element.style !== null) {
node.setFormat(element.style.textAlign as ElementFormatType);
}
}
return {node};
}
function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
const node = $createQuoteNode();
if (element.style !== null) {
node.setFormat(element.style.textAlign as ElementFormatType);
}
return {node};
}
export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode {
return $applyNodeReplacement(new HeadingNode(headingTag));
}
export function $isHeadingNode(
node: LexicalNode | null | undefined,
): node is HeadingNode {
return node instanceof HeadingNode;
}
function onPasteForRichText( function onPasteForRichText(
event: CommandPayloadType<typeof PASTE_COMMAND>, event: CommandPayloadType<typeof PASTE_COMMAND>,
@ -651,9 +299,6 @@ export function registerRichText(editor: LexicalEditor): () => void {
(parentNode): parentNode is ElementNode => (parentNode): parentNode is ElementNode =>
$isElementNode(parentNode) && !parentNode.isInline(), $isElementNode(parentNode) && !parentNode.isInline(),
); );
if (element !== null) {
element.setFormat(format);
}
} }
return true; return true;
}, },
@ -691,28 +336,6 @@ export function registerRichText(editor: LexicalEditor): () => void {
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR,
), ),
editor.registerCommand(
INDENT_CONTENT_COMMAND,
() => {
return $handleIndentAndOutdent((block) => {
const indent = block.getIndent();
block.setIndent(indent + 1);
});
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
OUTDENT_CONTENT_COMMAND,
() => {
return $handleIndentAndOutdent((block) => {
const indent = block.getIndent();
if (indent > 0) {
block.setIndent(indent - 1);
}
});
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<KeyboardEvent>( editor.registerCommand<KeyboardEvent>(
KEY_ARROW_UP_COMMAND, KEY_ARROW_UP_COMMAND,
(event) => { (event) => {
@ -846,19 +469,7 @@ export function registerRichText(editor: LexicalEditor): () => void {
return false; return false;
} }
event.preventDefault(); event.preventDefault();
const {anchor} = selection;
const anchorNode = anchor.getNode();
if (
selection.isCollapsed() &&
anchor.offset === 0 &&
!$isRootNode(anchorNode)
) {
const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
if (element.getIndent() > 0) {
return editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined);
}
}
return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true); return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true);
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR,

View File

@ -8,7 +8,7 @@
import {$createLinkNode} from '@lexical/link'; import {$createLinkNode} from '@lexical/link';
import {$createListItemNode, $createListNode} from '@lexical/list'; import {$createListItemNode, $createListNode} from '@lexical/list';
import {$createHeadingNode, registerRichText} from '@lexical/rich-text'; import {registerRichText} from '@lexical/rich-text';
import { import {
$addNodeStyle, $addNodeStyle,
$getSelectionStyleValueForProperty, $getSelectionStyleValueForProperty,
@ -74,6 +74,7 @@ import {
} from '../utils'; } from '../utils';
import {createEmptyHistoryState, registerHistory} from "@lexical/history"; import {createEmptyHistoryState, registerHistory} from "@lexical/history";
import {mergeRegister} from "@lexical/utils"; import {mergeRegister} from "@lexical/utils";
import {$createHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
interface ExpectedSelection { interface ExpectedSelection {
anchorPath: number[]; anchorPath: number[];
@ -2604,7 +2605,7 @@ describe('LexicalSelection tests', () => {
return $createHeadingNode('h1'); return $createHeadingNode('h1');
}); });
expect(JSON.stringify(testEditor._pendingEditorState?.toJSON())).toBe( expect(JSON.stringify(testEditor._pendingEditorState?.toJSON())).toBe(
'{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[{"children":[{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":3,"rowSpan":1},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1}],"direction":null,"format":"","indent":0,"type":"tablerow","version":1}],"direction":null,"format":"","indent":0,"type":"table","version":1},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"}],"direction":null,"format":"","indent":0,"type":"root","version":1}}', '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"},{"children":[{"children":[{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"},{"children":[],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"},{"children":[],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"}],"direction":null,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":3,"rowSpan":1,"styles":{},"alignment":""},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"},{"children":[],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"},{"children":[],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"}],"direction":null,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1,"styles":{},"alignment":""}],"direction":null,"type":"tablerow","version":1,"styles":{},"height":0}],"direction":null,"type":"table","version":1,"id":"","alignment":"","inset":0,"colWidths":[],"styles":{}},{"children":[],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"}],"direction":null,"type":"root","version":1}}',
); );
}); });
}); });
@ -2694,7 +2695,7 @@ describe('LexicalSelection tests', () => {
}); });
}); });
expect(element.innerHTML).toStrictEqual( expect(element.innerHTML).toStrictEqual(
`<h1><span data-lexical-text="true">1</span></h1><h1 style="padding-inline-start: calc(1 * 40px);"><span data-lexical-text="true">1.1</span></h1>`, `<h1><span data-lexical-text="true">1</span></h1><ul><li value="1"><h1><span data-lexical-text="true">1.1</span></h1></li></ul>`,
); );
}); });
@ -2733,7 +2734,7 @@ describe('LexicalSelection tests', () => {
}); });
}); });
expect(element.innerHTML).toStrictEqual( expect(element.innerHTML).toStrictEqual(
`<h1 style="padding-inline-start: calc(1 * 40px);"><span data-lexical-text="true">1.1</span></h1>`, `<ul><li value="1"><h1><span data-lexical-text="true">1.1</span></h1></li></ul>`,
); );
}); });
}); });

View File

@ -7,7 +7,6 @@
*/ */
import {$createLinkNode} from '@lexical/link'; import {$createLinkNode} from '@lexical/link';
import {$createHeadingNode, $isHeadingNode} from '@lexical/rich-text';
import { import {
$getSelectionStyleValueForProperty, $getSelectionStyleValueForProperty,
$patchStyleText, $patchStyleText,
@ -44,6 +43,7 @@ import {
} from 'lexical/__tests__/utils'; } from 'lexical/__tests__/utils';
import {$setAnchorPoint, $setFocusPoint} from '../utils'; import {$setAnchorPoint, $setFocusPoint} from '../utils';
import {$createHeadingNode, $isHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
Range.prototype.getBoundingClientRect = function (): DOMRect { Range.prototype.getBoundingClientRect = function (): DOMRect {
const rect = { const rect = {

View File

@ -81,8 +81,6 @@ export function $setBlocksType(
invariant($isElementNode(node), 'Expected block node to be an ElementNode'); invariant($isElementNode(node), 'Expected block node to be an ElementNode');
const targetElement = createElement(); const targetElement = createElement();
targetElement.setFormat(node.getFormatType());
targetElement.setIndent(node.getIndent());
node.replace(targetElement, true); node.replace(targetElement, true);
} }
} }
@ -136,8 +134,6 @@ export function $wrapNodes(
: anchor.getNode(); : anchor.getNode();
const children = target.getChildren(); const children = target.getChildren();
let element = createElement(); let element = createElement();
element.setFormat(target.getFormatType());
element.setIndent(target.getIndent());
children.forEach((child) => element.append(child)); children.forEach((child) => element.append(child));
if (wrappingElement) { if (wrappingElement) {
@ -277,8 +273,6 @@ export function $wrapNodesImpl(
if (elementMapping.get(parentKey) === undefined) { if (elementMapping.get(parentKey) === undefined) {
const targetElement = createElement(); const targetElement = createElement();
targetElement.setFormat(parent.getFormatType());
targetElement.setIndent(parent.getIndent());
elements.push(targetElement); elements.push(targetElement);
elementMapping.set(parentKey, targetElement); elementMapping.set(parentKey, targetElement);
// Move node and its siblings to the new // Move node and its siblings to the new
@ -299,8 +293,6 @@ export function $wrapNodesImpl(
'Expected node in emptyElements to be an ElementNode', 'Expected node in emptyElements to be an ElementNode',
); );
const targetElement = createElement(); const targetElement = createElement();
targetElement.setFormat(node.getFormatType());
targetElement.setIndent(node.getIndent());
elements.push(targetElement); elements.push(targetElement);
node.remove(true); node.remove(true);
} }

View File

@ -28,7 +28,8 @@ import {
ElementNode, ElementNode,
} from 'lexical'; } from 'lexical';
import {COLUMN_WIDTH, PIXEL_VALUE_REG_EXP} from './constants'; import {extractStyleMapFromElement, StyleMap} from "../../utils/dom";
import {CommonBlockAlignment, extractAlignmentFromElement} from "lexical/nodes/common";
export const TableCellHeaderStates = { export const TableCellHeaderStates = {
BOTH: 3, BOTH: 3,
@ -47,6 +48,8 @@ export type SerializedTableCellNode = Spread<
headerState: TableCellHeaderState; headerState: TableCellHeaderState;
width?: number; width?: number;
backgroundColor?: null | string; backgroundColor?: null | string;
styles: Record<string, string>;
alignment: CommonBlockAlignment;
}, },
SerializedElementNode SerializedElementNode
>; >;
@ -63,6 +66,10 @@ export class TableCellNode extends ElementNode {
__width?: number; __width?: number;
/** @internal */ /** @internal */
__backgroundColor: null | string; __backgroundColor: null | string;
/** @internal */
__styles: StyleMap = new Map;
/** @internal */
__alignment: CommonBlockAlignment = '';
static getType(): string { static getType(): string {
return 'tablecell'; return 'tablecell';
@ -77,6 +84,8 @@ export class TableCellNode extends ElementNode {
); );
cellNode.__rowSpan = node.__rowSpan; cellNode.__rowSpan = node.__rowSpan;
cellNode.__backgroundColor = node.__backgroundColor; cellNode.__backgroundColor = node.__backgroundColor;
cellNode.__styles = new Map(node.__styles);
cellNode.__alignment = node.__alignment;
return cellNode; return cellNode;
} }
@ -94,16 +103,20 @@ export class TableCellNode extends ElementNode {
} }
static importJSON(serializedNode: SerializedTableCellNode): TableCellNode { static importJSON(serializedNode: SerializedTableCellNode): TableCellNode {
const colSpan = serializedNode.colSpan || 1; const node = $createTableCellNode(
const rowSpan = serializedNode.rowSpan || 1;
const cellNode = $createTableCellNode(
serializedNode.headerState, serializedNode.headerState,
colSpan, serializedNode.colSpan,
serializedNode.width || undefined, serializedNode.width,
); );
cellNode.__rowSpan = rowSpan;
cellNode.__backgroundColor = serializedNode.backgroundColor || null; if (serializedNode.rowSpan) {
return cellNode; node.setRowSpan(serializedNode.rowSpan);
}
node.setStyles(new Map(Object.entries(serializedNode.styles)));
node.setAlignment(serializedNode.alignment);
return node;
} }
constructor( constructor(
@ -144,34 +157,19 @@ export class TableCellNode extends ElementNode {
this.hasHeader() && config.theme.tableCellHeader, this.hasHeader() && config.theme.tableCellHeader,
); );
for (const [name, value] of this.__styles.entries()) {
element.style.setProperty(name, value);
}
if (this.__alignment) {
element.classList.add('align-' + this.__alignment);
}
return element; return element;
} }
exportDOM(editor: LexicalEditor): DOMExportOutput { exportDOM(editor: LexicalEditor): DOMExportOutput {
const {element} = super.exportDOM(editor); const {element} = super.exportDOM(editor);
if (element) {
const element_ = element as HTMLTableCellElement;
element_.style.border = '1px solid black';
if (this.__colSpan > 1) {
element_.colSpan = this.__colSpan;
}
if (this.__rowSpan > 1) {
element_.rowSpan = this.__rowSpan;
}
element_.style.width = `${this.getWidth() || COLUMN_WIDTH}px`;
element_.style.verticalAlign = 'top';
element_.style.textAlign = 'start';
const backgroundColor = this.getBackgroundColor();
if (backgroundColor !== null) {
element_.style.backgroundColor = backgroundColor;
} else if (this.hasHeader()) {
element_.style.backgroundColor = '#f2f3f5';
}
}
return { return {
element, element,
}; };
@ -186,6 +184,8 @@ export class TableCellNode extends ElementNode {
rowSpan: this.__rowSpan, rowSpan: this.__rowSpan,
type: 'tablecell', type: 'tablecell',
width: this.getWidth(), width: this.getWidth(),
styles: Object.fromEntries(this.__styles),
alignment: this.__alignment,
}; };
} }
@ -231,6 +231,38 @@ export class TableCellNode extends ElementNode {
return this.getLatest().__width; return this.getLatest().__width;
} }
clearWidth(): void {
const self = this.getWritable();
self.__width = undefined;
}
getStyles(): StyleMap {
const self = this.getLatest();
return new Map(self.__styles);
}
setStyles(styles: StyleMap): void {
const self = this.getWritable();
self.__styles = new Map(styles);
}
setAlignment(alignment: CommonBlockAlignment) {
const self = this.getWritable();
self.__alignment = alignment;
}
getAlignment(): CommonBlockAlignment {
const self = this.getLatest();
return self.__alignment;
}
updateTag(tag: string): void {
const isHeader = tag.toLowerCase() === 'th';
const state = isHeader ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS;
const self = this.getWritable();
self.__headerState = state;
}
getBackgroundColor(): null | string { getBackgroundColor(): null | string {
return this.getLatest().__backgroundColor; return this.getLatest().__backgroundColor;
} }
@ -265,7 +297,9 @@ export class TableCellNode extends ElementNode {
prevNode.__width !== this.__width || prevNode.__width !== this.__width ||
prevNode.__colSpan !== this.__colSpan || prevNode.__colSpan !== this.__colSpan ||
prevNode.__rowSpan !== this.__rowSpan || prevNode.__rowSpan !== this.__rowSpan ||
prevNode.__backgroundColor !== this.__backgroundColor prevNode.__backgroundColor !== this.__backgroundColor ||
prevNode.__styles !== this.__styles ||
prevNode.__alignment !== this.__alignment
); );
} }
@ -294,6 +328,8 @@ export function $convertTableCellNodeElement(
let width: number | undefined = undefined; let width: number | undefined = undefined;
const PIXEL_VALUE_REG_EXP = /^(\d+(?:\.\d+)?)px$/;
if (PIXEL_VALUE_REG_EXP.test(domNode_.style.width)) { if (PIXEL_VALUE_REG_EXP.test(domNode_.style.width)) {
width = parseFloat(domNode_.style.width); width = parseFloat(domNode_.style.width);
} }
@ -307,10 +343,6 @@ export function $convertTableCellNodeElement(
); );
tableCellNode.__rowSpan = domNode_.rowSpan; tableCellNode.__rowSpan = domNode_.rowSpan;
const backgroundColor = domNode_.style.backgroundColor;
if (backgroundColor !== '') {
tableCellNode.__backgroundColor = backgroundColor;
}
const style = domNode_.style; const style = domNode_.style;
const textDecoration = style.textDecoration.split(' '); const textDecoration = style.textDecoration.split(' ');
@ -319,6 +351,12 @@ export function $convertTableCellNodeElement(
const hasLinethroughTextDecoration = textDecoration.includes('line-through'); const hasLinethroughTextDecoration = textDecoration.includes('line-through');
const hasItalicFontStyle = style.fontStyle === 'italic'; const hasItalicFontStyle = style.fontStyle === 'italic';
const hasUnderlineTextDecoration = textDecoration.includes('underline'); const hasUnderlineTextDecoration = textDecoration.includes('underline');
if (domNode instanceof HTMLElement) {
tableCellNode.setStyles(extractStyleMapFromElement(domNode));
tableCellNode.setAlignment(extractAlignmentFromElement(domNode));
}
return { return {
after: (childLexicalNodes) => { after: (childLexicalNodes) => {
if (childLexicalNodes.length === 0) { if (childLexicalNodes.length === 0) {
@ -360,7 +398,7 @@ export function $convertTableCellNodeElement(
} }
export function $createTableCellNode( export function $createTableCellNode(
headerState: TableCellHeaderState, headerState: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,
colSpan = 1, colSpan = 1,
width?: number, width?: number,
): TableCellNode { ): TableCellNode {

View File

@ -7,7 +7,7 @@
*/ */
import type {TableCellNode} from './LexicalTableCellNode'; import type {TableCellNode} from './LexicalTableCellNode';
import type { import {
DOMConversionMap, DOMConversionMap,
DOMConversionOutput, DOMConversionOutput,
DOMExportOutput, DOMExportOutput,
@ -15,31 +15,48 @@ import type {
LexicalEditor, LexicalEditor,
LexicalNode, LexicalNode,
NodeKey, NodeKey,
SerializedElementNode, Spread,
} from 'lexical'; } from 'lexical';
import {addClassNamesToElement, isHTMLElement} from '@lexical/utils'; import {addClassNamesToElement, isHTMLElement} from '@lexical/utils';
import { import {
$applyNodeReplacement, $applyNodeReplacement,
$getNearestNodeFromDOMNode, $getNearestNodeFromDOMNode,
ElementNode,
} from 'lexical'; } from 'lexical';
import {$isTableCellNode} from './LexicalTableCellNode'; import {$isTableCellNode} from './LexicalTableCellNode';
import {TableDOMCell, TableDOMTable} from './LexicalTableObserver'; import {TableDOMCell, TableDOMTable} from './LexicalTableObserver';
import {$isTableRowNode, TableRowNode} from './LexicalTableRowNode';
import {getTable} from './LexicalTableSelectionHelpers'; import {getTable} from './LexicalTableSelectionHelpers';
import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
import {
commonPropertiesDifferent, deserializeCommonBlockNode,
setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps
} from "lexical/nodes/common";
import {el, extractStyleMapFromElement, StyleMap} from "../../utils/dom";
import {getTableColumnWidths} from "../../utils/tables";
export type SerializedTableNode = SerializedElementNode; export type SerializedTableNode = Spread<{
colWidths: string[];
styles: Record<string, string>,
}, SerializedCommonBlockNode>
/** @noInheritDoc */ /** @noInheritDoc */
export class TableNode extends ElementNode { export class TableNode extends CommonBlockNode {
__colWidths: string[] = [];
__styles: StyleMap = new Map;
static getType(): string { static getType(): string {
return 'table'; return 'table';
} }
static clone(node: TableNode): TableNode { static clone(node: TableNode): TableNode {
return new TableNode(node.__key); const newNode = new TableNode(node.__key);
copyCommonBlockProperties(node, newNode);
newNode.__colWidths = node.__colWidths;
newNode.__styles = new Map(node.__styles);
return newNode;
} }
static importDOM(): DOMConversionMap | null { static importDOM(): DOMConversionMap | null {
@ -52,18 +69,24 @@ export class TableNode extends ElementNode {
} }
static importJSON(_serializedNode: SerializedTableNode): TableNode { static importJSON(_serializedNode: SerializedTableNode): TableNode {
return $createTableNode(); const node = $createTableNode();
deserializeCommonBlockNode(_serializedNode, node);
node.setColWidths(_serializedNode.colWidths);
node.setStyles(new Map(Object.entries(_serializedNode.styles)));
return node;
} }
constructor(key?: NodeKey) { constructor(key?: NodeKey) {
super(key); super(key);
} }
exportJSON(): SerializedElementNode { exportJSON(): SerializedTableNode {
return { return {
...super.exportJSON(), ...super.exportJSON(),
type: 'table', type: 'table',
version: 1, version: 1,
colWidths: this.__colWidths,
styles: Object.fromEntries(this.__styles),
}; };
} }
@ -72,11 +95,33 @@ export class TableNode extends ElementNode {
addClassNamesToElement(tableElement, config.theme.table); addClassNamesToElement(tableElement, config.theme.table);
updateElementWithCommonBlockProps(tableElement, this);
const colWidths = this.getColWidths();
if (colWidths.length > 0) {
const colgroup = el('colgroup');
for (const width of colWidths) {
const col = el('col');
if (width) {
col.style.width = width;
}
colgroup.append(col);
}
tableElement.append(colgroup);
}
for (const [name, value] of this.__styles.entries()) {
tableElement.style.setProperty(name, value);
}
return tableElement; return tableElement;
} }
updateDOM(): boolean { updateDOM(_prevNode: TableNode): boolean {
return false; return commonPropertiesDifferent(_prevNode, this)
|| this.__colWidths.join(':') !== _prevNode.__colWidths.join(':')
|| this.__styles.size !== _prevNode.__styles.size
|| (Array.from(this.__styles.values()).join(':') !== (Array.from(_prevNode.__styles.values()).join(':')));
} }
exportDOM(editor: LexicalEditor): DOMExportOutput { exportDOM(editor: LexicalEditor): DOMExportOutput {
@ -115,6 +160,26 @@ export class TableNode extends ElementNode {
return true; return true;
} }
setColWidths(widths: string[]) {
const self = this.getWritable();
self.__colWidths = widths;
}
getColWidths(): string[] {
const self = this.getLatest();
return self.__colWidths;
}
getStyles(): StyleMap {
const self = this.getLatest();
return new Map(self.__styles);
}
setStyles(styles: StyleMap): void {
const self = this.getWritable();
self.__styles = new Map(styles);
}
getCordsFromCellNode( getCordsFromCellNode(
tableCellNode: TableCellNode, tableCellNode: TableCellNode,
table: TableDOMTable, table: TableDOMTable,
@ -239,8 +304,15 @@ export function $getElementForTableNode(
return getTable(tableElement); return getTable(tableElement);
} }
export function $convertTableElement(_domNode: Node): DOMConversionOutput { export function $convertTableElement(element: HTMLElement): DOMConversionOutput {
return {node: $createTableNode()}; const node = $createTableNode();
setCommonBlockPropsFromElement(element, node);
const colWidths = getTableColumnWidths(element as HTMLTableElement);
node.setColWidths(colWidths);
node.setStyles(extractStyleMapFromElement(element));
return {node};
} }
export function $createTableNode(): TableNode { export function $createTableNode(): TableNode {

View File

@ -20,11 +20,12 @@ import {
SerializedElementNode, SerializedElementNode,
} from 'lexical'; } from 'lexical';
import {PIXEL_VALUE_REG_EXP} from './constants'; import {extractStyleMapFromElement, sizeToPixels, StyleMap} from "../../utils/dom";
export type SerializedTableRowNode = Spread< export type SerializedTableRowNode = Spread<
{ {
height?: number; styles: Record<string, string>,
height?: number,
}, },
SerializedElementNode SerializedElementNode
>; >;
@ -33,13 +34,17 @@ export type SerializedTableRowNode = Spread<
export class TableRowNode extends ElementNode { export class TableRowNode extends ElementNode {
/** @internal */ /** @internal */
__height?: number; __height?: number;
/** @internal */
__styles: StyleMap = new Map();
static getType(): string { static getType(): string {
return 'tablerow'; return 'tablerow';
} }
static clone(node: TableRowNode): TableRowNode { static clone(node: TableRowNode): TableRowNode {
return new TableRowNode(node.__height, node.__key); const newNode = new TableRowNode(node.__key);
newNode.__styles = new Map(node.__styles);
return newNode;
} }
static importDOM(): DOMConversionMap | null { static importDOM(): DOMConversionMap | null {
@ -52,20 +57,24 @@ export class TableRowNode extends ElementNode {
} }
static importJSON(serializedNode: SerializedTableRowNode): TableRowNode { static importJSON(serializedNode: SerializedTableRowNode): TableRowNode {
return $createTableRowNode(serializedNode.height); const node = $createTableRowNode();
node.setStyles(new Map(Object.entries(serializedNode.styles)));
return node;
} }
constructor(height?: number, key?: NodeKey) { constructor(key?: NodeKey) {
super(key); super(key);
this.__height = height;
} }
exportJSON(): SerializedTableRowNode { exportJSON(): SerializedTableRowNode {
return { return {
...super.exportJSON(), ...super.exportJSON(),
...(this.getHeight() && {height: this.getHeight()}),
type: 'tablerow', type: 'tablerow',
version: 1, version: 1,
styles: Object.fromEntries(this.__styles),
height: this.__height || 0,
}; };
} }
@ -76,6 +85,10 @@ export class TableRowNode extends ElementNode {
element.style.height = `${this.__height}px`; element.style.height = `${this.__height}px`;
} }
for (const [name, value] of this.__styles.entries()) {
element.style.setProperty(name, value);
}
addClassNamesToElement(element, config.theme.tableRow); addClassNamesToElement(element, config.theme.tableRow);
return element; return element;
@ -85,6 +98,16 @@ export class TableRowNode extends ElementNode {
return true; return true;
} }
getStyles(): StyleMap {
const self = this.getLatest();
return new Map(self.__styles);
}
setStyles(styles: StyleMap): void {
const self = this.getWritable();
self.__styles = new Map(styles);
}
setHeight(height: number): number | null | undefined { setHeight(height: number): number | null | undefined {
const self = this.getWritable(); const self = this.getWritable();
self.__height = height; self.__height = height;
@ -96,7 +119,8 @@ export class TableRowNode extends ElementNode {
} }
updateDOM(prevNode: TableRowNode): boolean { updateDOM(prevNode: TableRowNode): boolean {
return prevNode.__height !== this.__height; return prevNode.__height !== this.__height
|| prevNode.__styles !== this.__styles;
} }
canBeEmpty(): false { canBeEmpty(): false {
@ -109,18 +133,21 @@ export class TableRowNode extends ElementNode {
} }
export function $convertTableRowElement(domNode: Node): DOMConversionOutput { export function $convertTableRowElement(domNode: Node): DOMConversionOutput {
const domNode_ = domNode as HTMLTableCellElement; const rowNode = $createTableRowNode();
let height: number | undefined = undefined; const domNode_ = domNode as HTMLElement;
if (PIXEL_VALUE_REG_EXP.test(domNode_.style.height)) { const height = sizeToPixels(domNode_.style.height);
height = parseFloat(domNode_.style.height); rowNode.setHeight(height);
if (domNode instanceof HTMLElement) {
rowNode.setStyles(extractStyleMapFromElement(domNode));
} }
return {node: $createTableRowNode(height)}; return {node: rowNode};
} }
export function $createTableRowNode(height?: number): TableRowNode { export function $createTableRowNode(): TableRowNode {
return $applyNodeReplacement(new TableRowNode(height)); return $applyNodeReplacement(new TableRowNode());
} }
export function $isTableRowNode( export function $isTableRowNode(

View File

@ -16,7 +16,6 @@ import type {
} from './LexicalTableSelection'; } from './LexicalTableSelection';
import type { import type {
BaseSelection, BaseSelection,
ElementFormatType,
LexicalCommand, LexicalCommand,
LexicalEditor, LexicalEditor,
LexicalNode, LexicalNode,
@ -50,7 +49,6 @@ import {
DELETE_LINE_COMMAND, DELETE_LINE_COMMAND,
DELETE_WORD_COMMAND, DELETE_WORD_COMMAND,
FOCUS_COMMAND, FOCUS_COMMAND,
FORMAT_ELEMENT_COMMAND,
FORMAT_TEXT_COMMAND, FORMAT_TEXT_COMMAND,
INSERT_PARAGRAPH_COMMAND, INSERT_PARAGRAPH_COMMAND,
KEY_ARROW_DOWN_COMMAND, KEY_ARROW_DOWN_COMMAND,
@ -438,59 +436,6 @@ export function applyTableHandlers(
), ),
); );
tableObserver.listenersToRemove.add(
editor.registerCommand<ElementFormatType>(
FORMAT_ELEMENT_COMMAND,
(formatType) => {
const selection = $getSelection();
if (
!$isTableSelection(selection) ||
!$isSelectionInTable(selection, tableNode)
) {
return false;
}
const anchorNode = selection.anchor.getNode();
const focusNode = selection.focus.getNode();
if (!$isTableCellNode(anchorNode) || !$isTableCellNode(focusNode)) {
return false;
}
const [tableMap, anchorCell, focusCell] = $computeTableMap(
tableNode,
anchorNode,
focusNode,
);
const maxRow = Math.max(anchorCell.startRow, focusCell.startRow);
const maxColumn = Math.max(
anchorCell.startColumn,
focusCell.startColumn,
);
const minRow = Math.min(anchorCell.startRow, focusCell.startRow);
const minColumn = Math.min(
anchorCell.startColumn,
focusCell.startColumn,
);
for (let i = minRow; i <= maxRow; i++) {
for (let j = minColumn; j <= maxColumn; j++) {
const cell = tableMap[i][j].cell;
cell.setFormat(formatType);
const cellChildren = cell.getChildren();
for (let k = 0; k < cellChildren.length; k++) {
const child = cellChildren[k];
if ($isElementNode(child) && !child.isInline()) {
child.setFormat(formatType);
}
}
}
}
return true;
},
COMMAND_PRIORITY_CRITICAL,
),
);
tableObserver.listenersToRemove.add( tableObserver.listenersToRemove.add(
editor.registerCommand( editor.registerCommand(
CONTROLLED_TEXT_INSERTION_COMMAND, CONTROLLED_TEXT_INSERTION_COMMAND,

View File

@ -113,9 +113,8 @@ describe('LexicalTableNode tests', () => {
$insertDataTransferForRichText(dataTransfer, selection, editor); $insertDataTransferForRichText(dataTransfer, selection, editor);
}); });
// Make sure paragraph is inserted inside empty cells // Make sure paragraph is inserted inside empty cells
const emptyCell = '<td><p><br></p></td>';
expect(testEnv.innerHTML).toBe( expect(testEnv.innerHTML).toBe(
`<table><tr><td><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Hello there</span></p></td><td><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">General Kenobi!</span></p></td></tr><tr><td><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Lexical is nice</span></p></td>${emptyCell}</tr></table>`, `<table style="border-collapse: collapse; table-layout: fixed; width: 468pt;"><colgroup><col><col></colgroup><tr style="height: 22.015pt;"><td style="border-left: 1pt solid #000000; border-right: 1pt solid #000000; border-bottom: 1pt solid #000000; border-top: 1pt solid #000000; vertical-align: top; padding: 5pt 5pt 5pt 5pt; overflow: hidden; overflow-wrap: break-word;"><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Hello there</span></p></td><td style="border-left: 1pt solid #000000; border-right: 1pt solid #000000; border-bottom: 1pt solid #000000; border-top: 1pt solid #000000; vertical-align: top; padding: 5pt 5pt 5pt 5pt; overflow: hidden; overflow-wrap: break-word;"><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">General Kenobi!</span></p></td></tr><tr style="height: 22.015pt;"><td style="border-left: 1pt solid #000000; border-right: 1pt solid #000000; border-bottom: 1pt solid #000000; border-top: 1pt solid #000000; vertical-align: top; padding: 5pt 5pt 5pt 5pt; overflow: hidden; overflow-wrap: break-word;"><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Lexical is nice</span></p></td><td style="border-left: 1pt solid #000000; border-right: 1pt solid #000000; border-bottom: 1pt solid #000000; border-top: 1pt solid #000000; vertical-align: top; padding: 5pt 5pt 5pt 5pt; overflow: hidden; overflow-wrap: break-word;"><p><br></p></td></tr></table>`,
); );
}); });
@ -136,7 +135,7 @@ describe('LexicalTableNode tests', () => {
$insertDataTransferForRichText(dataTransfer, selection, editor); $insertDataTransferForRichText(dataTransfer, selection, editor);
}); });
expect(testEnv.innerHTML).toBe( expect(testEnv.innerHTML).toBe(
`<table><tr style="height: 21px;"><td><p><strong data-lexical-text="true">Surface</strong></p></td><td><p><em data-lexical-text="true">MWP_WORK_LS_COMPOSER</em></p></td><td><p style="text-align: right;"><span data-lexical-text="true">77349</span></p></td></tr><tr style="height: 21px;"><td><p><span data-lexical-text="true">Lexical</span></p></td><td><p><span data-lexical-text="true">XDS_RICH_TEXT_AREA</span></p></td><td><p><span data-lexical-text="true">sdvd </span><strong data-lexical-text="true">sdfvsfs</strong></p></td></tr></table>`, `<table style="table-layout: fixed; font-size: 10pt; font-family: Arial; width: 0px; border-collapse: collapse;"><colgroup><col style="width: 100px;"><col style="width: 189px;"><col style="width: 171px;"></colgroup><tr style="height: 21px;"><td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom; font-weight: bold;"><p><strong data-lexical-text="true">Surface</strong></p></td><td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom; font-style: italic;"><p><em data-lexical-text="true">MWP_WORK_LS_COMPOSER</em></p></td><td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom; text-decoration: underline; text-align: right;" class="align-right"><p><span data-lexical-text="true">77349</span></p></td></tr><tr style="height: 21px;"><td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;"><p><span data-lexical-text="true">Lexical</span></p></td><td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom; text-decoration: line-through;"><p><span data-lexical-text="true">XDS_RICH_TEXT_AREA</span></p></td><td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;"><p><span data-lexical-text="true">sdvd </span><strong data-lexical-text="true">sdfvsfs</strong></p></td></tr></table>`,
); );
}); });
}, },

View File

@ -39,10 +39,9 @@ describe('LexicalTableRowNode tests', () => {
`<tr class="${editorConfig.theme.tableRow}"></tr>`, `<tr class="${editorConfig.theme.tableRow}"></tr>`,
); );
const rowHeight = 36; const rowWithCustomHeightNode = $createTableRowNode();
const rowWithCustomHeightNode = $createTableRowNode(36);
expect(rowWithCustomHeightNode.createDOM(editorConfig).outerHTML).toBe( expect(rowWithCustomHeightNode.createDOM(editorConfig).outerHTML).toBe(
`<tr style="height: ${rowHeight}px;" class="${editorConfig.theme.tableRow}"></tr>`, `<tr class="${editorConfig.theme.tableRow}"></tr>`,
); );
}); });
}); });

View File

@ -101,8 +101,6 @@ describe('table selection', () => {
__cachedText: null, __cachedText: null,
__dir: null, __dir: null,
__first: paragraphKey, __first: paragraphKey,
__format: 0,
__indent: 0,
__key: 'root', __key: 'root',
__last: paragraphKey, __last: paragraphKey,
__next: null, __next: null,
@ -113,10 +111,11 @@ describe('table selection', () => {
__type: 'root', __type: 'root',
}); });
expect(parsedParagraph).toEqual({ expect(parsedParagraph).toEqual({
__alignment: "",
__dir: null, __dir: null,
__first: textKey, __first: textKey,
__format: 0, __id: '',
__indent: 0, __inset: 0,
__key: paragraphKey, __key: paragraphKey,
__last: textKey, __last: textKey,
__next: null, __next: null,
@ -124,7 +123,6 @@ describe('table selection', () => {
__prev: null, __prev: null,
__size: 1, __size: 1,
__style: '', __style: '',
__textFormat: 0,
__textStyle: '', __textStyle: '',
__type: 'paragraph', __type: 'paragraph',
}); });

View File

@ -7,7 +7,7 @@
*/ */
import {AutoLinkNode, LinkNode} from '@lexical/link'; import {AutoLinkNode, LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list'; import {ListItemNode, ListNode} from '@lexical/list';
import {HeadingNode, QuoteNode, registerRichText} from '@lexical/rich-text'; import {registerRichText} from '@lexical/rich-text';
import { import {
applySelectionInputs, applySelectionInputs,
pasteHTML, pasteHTML,
@ -15,6 +15,8 @@ import {
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table'; import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
import {$createParagraphNode, $insertNodes, LexicalEditor} from 'lexical'; import {$createParagraphNode, $insertNodes, LexicalEditor} from 'lexical';
import {createTestEditor, initializeClipboard} from 'lexical/__tests__/utils'; import {createTestEditor, initializeClipboard} from 'lexical/__tests__/utils';
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
jest.mock('lexical/shared/environment', () => { jest.mock('lexical/shared/environment', () => {
const originalModule = jest.requireActual('lexical/shared/environment'); const originalModule = jest.requireActual('lexical/shared/environment');
@ -174,7 +176,7 @@ describe('LexicalEventHelpers', () => {
}, },
{ {
expectedHTML: expectedHTML:
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1" class="editor-listitem"><span data-lexical-text="true">Other side</span></li><li value="2" class="editor-listitem"><span data-lexical-text="true">I must have called</span></li></ul></div>', '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1"><span data-lexical-text="true">Other side</span></li><li value="2"><span data-lexical-text="true">I must have called</span></li></ul></div>',
inputs: [ inputs: [
pasteHTML( pasteHTML(
`<meta charset='utf-8'><ul><li>Other side</li><li>I must have called</li></ul>`, `<meta charset='utf-8'><ul><li>Other side</li><li>I must have called</li></ul>`,
@ -184,7 +186,7 @@ describe('LexicalEventHelpers', () => {
}, },
{ {
expectedHTML: expectedHTML:
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ol class="editor-list-ol"><li value="1" class="editor-listitem"><span data-lexical-text="true">To tell you</span></li><li value="2" class="editor-listitem"><span data-lexical-text="true">Im sorry</span></li></ol></div>', '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ol class="editor-list-ol"><li value="1"><span data-lexical-text="true">To tell you</span></li><li value="2"><span data-lexical-text="true">Im sorry</span></li></ol></div>',
inputs: [ inputs: [
pasteHTML( pasteHTML(
`<meta charset='utf-8'><ol><li>To tell you</li><li>Im sorry</li></ol>`, `<meta charset='utf-8'><ol><li>To tell you</li><li>Im sorry</li></ol>`,
@ -264,7 +266,7 @@ describe('LexicalEventHelpers', () => {
}, },
{ {
expectedHTML: expectedHTML:
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1" class="editor-listitem"><span data-lexical-text="true">Hello</span></li><li value="2" class="editor-listitem"><span data-lexical-text="true">from the other</span></li><li value="3" class="editor-listitem"><span data-lexical-text="true">side</span></li></ul></div>', '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1"><span data-lexical-text="true">Hello</span></li><li value="2"><span data-lexical-text="true">from the other</span></li><li value="3"><span data-lexical-text="true">side</span></li></ul></div>',
inputs: [ inputs: [
pasteHTML( pasteHTML(
`<meta charset='utf-8'><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist>`, `<meta charset='utf-8'><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist>`,
@ -274,7 +276,7 @@ describe('LexicalEventHelpers', () => {
}, },
{ {
expectedHTML: expectedHTML:
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1" class="editor-listitem"><span data-lexical-text="true">Hello</span></li><li value="2" class="editor-listitem"><span data-lexical-text="true">from the other</span></li><li value="3" class="editor-listitem"><span data-lexical-text="true">side</span></li></ul></div>', '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1"><span data-lexical-text="true">Hello</span></li><li value="2"><span data-lexical-text="true">from the other</span></li><li value="3"><span data-lexical-text="true">side</span></li></ul></div>',
inputs: [ inputs: [
pasteHTML( pasteHTML(
`<meta charset='utf-8'><doesnotexist><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist></doesnotexist>`, `<meta charset='utf-8'><doesnotexist><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist></doesnotexist>`,
@ -609,7 +611,7 @@ describe('LexicalEventHelpers', () => {
}, },
{ {
expectedHTML: expectedHTML:
'<ol class="editor-list-ol"><li value="1" class="editor-listitem"><span data-lexical-text="true">1</span><br><span data-lexical-text="true">2</span></li><li value="2" class="editor-listitem"><br></li><li value="3" class="editor-listitem"><span data-lexical-text="true">3</span></li></ol>', '<ol class="editor-list-ol"><li value="1"><span data-lexical-text="true">1</span><br><span data-lexical-text="true">2</span></li><li value="2"><br></li><li value="3"><span data-lexical-text="true">3</span></li></ol>',
inputs: [ inputs: [
pasteHTML('<ol><li>1<div></div>2</li><li></li><li>3</li></ol>'), pasteHTML('<ol><li>1<div></div>2</li><li></li><li>3</li></ol>'),
], ],
@ -645,7 +647,7 @@ describe('LexicalEventHelpers', () => {
}, },
{ {
expectedHTML: expectedHTML:
'<ol class="editor-list-ol"><li value="1" class="editor-listitem"><span data-lexical-text="true">1</span></li><li value="2" class="editor-listitem"><br></li><li value="3" class="editor-listitem"><span data-lexical-text="true">3</span></li></ol>', '<ol class="editor-list-ol"><li value="1"><span data-lexical-text="true">1</span></li><li value="2"><br></li><li value="3"><span data-lexical-text="true">3</span></li></ol>',
inputs: [pasteHTML('<ol><li>1</li><li><br /></li><li>3</li></ol>')], inputs: [pasteHTML('<ol><li>1</li><li><br /></li><li>3</li></ol>')],
name: 'only br in a li', name: 'only br in a li',
}, },

View File

@ -82,10 +82,10 @@ describe('LexicalUtils#splitNode', () => {
expectedHtml: expectedHtml:
'<ul>' + '<ul>' +
'<li>Before</li>' + '<li>Before</li>' +
'<li><ul><li>Hello</li></ul></li>' + '<li style="list-style: none;"><ul><li>Hello</li></ul></li>' +
'</ul>' + '</ul>' +
'<ul>' + '<ul>' +
'<li><ul><li>world</li></ul></li>' + '<li style="list-style: none;"><ul><li>world</li></ul></li>' +
'<li>After</li>' + '<li>After</li>' +
'</ul>', '</ul>',
initialHtml: initialHtml:

View File

@ -56,11 +56,11 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
expectedHtml: expectedHtml:
'<ul>' + '<ul>' +
'<li>Before</li>' + '<li>Before</li>' +
'<li><ul><li>Hello</li></ul></li>' + '<li style="list-style: none;"><ul><li>Hello</li></ul></li>' +
'</ul>' + '</ul>' +
'<test-decorator></test-decorator>' + '<test-decorator></test-decorator>' +
'<ul>' + '<ul>' +
'<li><ul><li>world</li></ul></li>' + '<li style="list-style: none;"><ul><li>world</li></ul></li>' +
'<li>After</li>' + '<li>After</li>' +
'</ul>', '</ul>',
initialHtml: initialHtml:

View File

@ -0,0 +1,67 @@
import {CalloutNode} from '@lexical/rich-text/LexicalCalloutNode';
import {
ElementNode,
KlassConstructor,
LexicalNode,
LexicalNodeReplacement, NodeMutation,
ParagraphNode
} from "lexical";
import {LinkNode} from "@lexical/link";
import {ImageNode} from "@lexical/rich-text/LexicalImageNode";
import {DetailsNode, SummaryNode} from "@lexical/rich-text/LexicalDetailsNode";
import {ListItemNode, ListNode} from "@lexical/list";
import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
import {HorizontalRuleNode} from "@lexical/rich-text/LexicalHorizontalRuleNode";
import {CodeBlockNode} from "@lexical/rich-text/LexicalCodeBlockNode";
import {DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
import {EditorUiContext} from "./ui/framework/core";
import {MediaNode} from "@lexical/rich-text/LexicalMediaNode";
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
/**
* Load the nodes for lexical.
*/
export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {
return [
CalloutNode,
HeadingNode,
QuoteNode,
ListNode,
ListItemNode,
TableNode,
TableRowNode,
TableCellNode,
ImageNode, // TODO - Alignment
HorizontalRuleNode,
DetailsNode, SummaryNode,
CodeBlockNode,
DiagramNode,
MediaNode, // TODO - Alignment
ParagraphNode,
LinkNode,
];
}
export function registerCommonNodeMutationListeners(context: EditorUiContext): void {
const decorated = [ImageNode, CodeBlockNode, DiagramNode];
const decorationDestroyListener = (mutations: Map<string, NodeMutation>): void => {
for (let [nodeKey, mutation] of mutations) {
if (mutation === "destroyed") {
const decorator = context.manager.getDecoratorByNodeKey(nodeKey);
if (decorator) {
decorator.destroy(context);
}
}
}
};
for (let decoratedNode of decorated) {
// Have to pass a unique function here since they are stored by lexical keyed on listener function.
context.editor.registerMutationListener(decoratedNode, (mutations) => decorationDestroyListener(mutations));
}
}
export type LexicalNodeMatcher = (node: LexicalNode|null|undefined) => boolean;
export type LexicalElementNodeCreator = () => ElementNode;

View File

@ -1,146 +0,0 @@
import {
DOMConversionMap,
DOMConversionOutput,
LexicalNode,
Spread
} from "lexical";
import {EditorConfig} from "lexical/LexicalEditor";
import {HeadingNode, HeadingTagType, SerializedHeadingNode} from "@lexical/rich-text";
import {
CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
SerializedCommonBlockNode,
setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps
} from "./_common";
export type SerializedCustomHeadingNode = Spread<SerializedCommonBlockNode, SerializedHeadingNode>
export class CustomHeadingNode extends HeadingNode {
__id: string = '';
__alignment: CommonBlockAlignment = '';
__inset: number = 0;
static getType() {
return 'custom-heading';
}
setId(id: string) {
const self = this.getWritable();
self.__id = id;
}
getId(): string {
const self = this.getLatest();
return self.__id;
}
setAlignment(alignment: CommonBlockAlignment) {
const self = this.getWritable();
self.__alignment = alignment;
}
getAlignment(): CommonBlockAlignment {
const self = this.getLatest();
return self.__alignment;
}
setInset(size: number) {
const self = this.getWritable();
self.__inset = size;
}
getInset(): number {
const self = this.getLatest();
return self.__inset;
}
static clone(node: CustomHeadingNode) {
const newNode = new CustomHeadingNode(node.__tag, node.__key);
newNode.__alignment = node.__alignment;
newNode.__inset = node.__inset;
return newNode;
}
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
updateElementWithCommonBlockProps(dom, this);
return dom;
}
updateDOM(prevNode: CustomHeadingNode, dom: HTMLElement): boolean {
return super.updateDOM(prevNode, dom)
|| commonPropertiesDifferent(prevNode, this);
}
exportJSON(): SerializedCustomHeadingNode {
return {
...super.exportJSON(),
type: 'custom-heading',
version: 1,
id: this.__id,
alignment: this.__alignment,
inset: this.__inset,
};
}
static importJSON(serializedNode: SerializedCustomHeadingNode): CustomHeadingNode {
const node = $createCustomHeadingNode(serializedNode.tag);
deserializeCommonBlockNode(serializedNode, node);
return node;
}
static importDOM(): DOMConversionMap | null {
return {
h1: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h2: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h3: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h4: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h5: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
h6: (node: Node) => ({
conversion: $convertHeadingElement,
priority: 0,
}),
};
}
}
function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
const nodeName = element.nodeName.toLowerCase();
let node = null;
if (
nodeName === 'h1' ||
nodeName === 'h2' ||
nodeName === 'h3' ||
nodeName === 'h4' ||
nodeName === 'h5' ||
nodeName === 'h6'
) {
node = $createCustomHeadingNode(nodeName);
setCommonBlockPropsFromElement(element, node);
}
return {node};
}
export function $createCustomHeadingNode(tag: HeadingTagType) {
return new CustomHeadingNode(tag);
}
export function $isCustomHeadingNode(node: LexicalNode | null | undefined): node is CustomHeadingNode {
return node instanceof CustomHeadingNode;
}

View File

@ -1,120 +0,0 @@
import {$isListNode, ListItemNode, SerializedListItemNode} from "@lexical/list";
import {EditorConfig} from "lexical/LexicalEditor";
import {DOMExportOutput, LexicalEditor, LexicalNode} from "lexical";
import {el} from "../utils/dom";
import {$isCustomListNode} from "./custom-list";
function updateListItemChecked(
dom: HTMLElement,
listItemNode: ListItemNode,
): void {
// Only set task list attrs for leaf list items
const shouldBeTaskItem = !$isListNode(listItemNode.getFirstChild());
dom.classList.toggle('task-list-item', shouldBeTaskItem);
if (listItemNode.__checked) {
dom.setAttribute('checked', 'checked');
} else {
dom.removeAttribute('checked');
}
}
export class CustomListItemNode extends ListItemNode {
static getType(): string {
return 'custom-list-item';
}
static clone(node: CustomListItemNode): CustomListItemNode {
return new CustomListItemNode(node.__value, node.__checked, node.__key);
}
createDOM(config: EditorConfig): HTMLElement {
const element = document.createElement('li');
const parent = this.getParent();
if ($isListNode(parent) && parent.getListType() === 'check') {
updateListItemChecked(element, this);
}
element.value = this.__value;
if ($hasNestedListWithoutLabel(this)) {
element.style.listStyle = 'none';
}
return element;
}
updateDOM(
prevNode: ListItemNode,
dom: HTMLElement,
config: EditorConfig,
): boolean {
const parent = this.getParent();
if ($isListNode(parent) && parent.getListType() === 'check') {
updateListItemChecked(dom, this);
}
dom.style.listStyle = $hasNestedListWithoutLabel(this) ? 'none' : '';
// @ts-expect-error - this is always HTMLListItemElement
dom.value = this.__value;
return false;
}
exportDOM(editor: LexicalEditor): DOMExportOutput {
const element = this.createDOM(editor._config);
element.style.textAlign = this.getFormatType();
if (element.classList.contains('task-list-item')) {
const input = el('input', {
type: 'checkbox',
disabled: 'disabled',
});
if (element.hasAttribute('checked')) {
input.setAttribute('checked', 'checked');
element.removeAttribute('checked');
}
element.prepend(input);
}
return {
element,
};
}
exportJSON(): SerializedListItemNode {
return {
...super.exportJSON(),
type: 'custom-list-item',
};
}
}
function $hasNestedListWithoutLabel(node: CustomListItemNode): boolean {
const children = node.getChildren();
let hasLabel = false;
let hasNestedList = false;
for (const child of children) {
if ($isCustomListNode(child)) {
hasNestedList = true;
} else if (child.getTextContent().trim().length > 0) {
hasLabel = true;
}
}
return hasNestedList && !hasLabel;
}
export function $isCustomListItemNode(
node: LexicalNode | null | undefined,
): node is CustomListItemNode {
return node instanceof CustomListItemNode;
}
export function $createCustomListItemNode(): CustomListItemNode {
return new CustomListItemNode();
}

View File

@ -1,139 +0,0 @@
import {
DOMConversionFn,
DOMConversionMap, EditorConfig,
LexicalNode,
Spread
} from "lexical";
import {$isListItemNode, ListItemNode, ListNode, ListType, SerializedListNode} from "@lexical/list";
import {$createCustomListItemNode} from "./custom-list-item";
import {extractDirectionFromElement} from "./_common";
export type SerializedCustomListNode = Spread<{
id: string;
}, SerializedListNode>
export class CustomListNode extends ListNode {
__id: string = '';
static getType() {
return 'custom-list';
}
setId(id: string) {
const self = this.getWritable();
self.__id = id;
}
getId(): string {
const self = this.getLatest();
return self.__id;
}
static clone(node: CustomListNode) {
const newNode = new CustomListNode(node.__listType, node.__start, node.__key);
newNode.__id = node.__id;
newNode.__dir = node.__dir;
return newNode;
}
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
if (this.__id) {
dom.setAttribute('id', this.__id);
}
if (this.__dir) {
dom.setAttribute('dir', this.__dir);
}
return dom;
}
updateDOM(prevNode: ListNode, dom: HTMLElement, config: EditorConfig): boolean {
return super.updateDOM(prevNode, dom, config) ||
prevNode.__dir !== this.__dir;
}
exportJSON(): SerializedCustomListNode {
return {
...super.exportJSON(),
type: 'custom-list',
version: 1,
id: this.__id,
};
}
static importJSON(serializedNode: SerializedCustomListNode): CustomListNode {
const node = $createCustomListNode(serializedNode.listType);
node.setId(serializedNode.id);
node.setDirection(serializedNode.direction);
return node;
}
static importDOM(): DOMConversionMap | null {
// @ts-ignore
const converter = super.importDOM().ol().conversion as DOMConversionFn<HTMLElement>;
const customConvertFunction = (element: HTMLElement) => {
const baseResult = converter(element);
if (element.id && baseResult?.node) {
(baseResult.node as CustomListNode).setId(element.id);
}
if (element.dir && baseResult?.node) {
(baseResult.node as CustomListNode).setDirection(extractDirectionFromElement(element));
}
if (baseResult) {
baseResult.after = $normalizeChildren;
}
return baseResult;
};
return {
ol: () => ({
conversion: customConvertFunction,
priority: 0,
}),
ul: () => ({
conversion: customConvertFunction,
priority: 0,
}),
};
}
}
/*
* This function is a custom normalization function to allow nested lists within list item elements.
* Original taken from https://github.com/facebook/lexical/blob/6e10210fd1e113ccfafdc999b1d896733c5c5bea/packages/lexical-list/src/LexicalListNode.ts#L284-L303
* With modifications made.
* Copyright (c) Meta Platforms, Inc. and affiliates.
* MIT license
*/
function $normalizeChildren(nodes: Array<LexicalNode>): Array<ListItemNode> {
const normalizedListItems: Array<ListItemNode> = [];
for (const node of nodes) {
if ($isListItemNode(node)) {
normalizedListItems.push(node);
} else {
normalizedListItems.push($wrapInListItem(node));
}
}
return normalizedListItems;
}
function $wrapInListItem(node: LexicalNode): ListItemNode {
const listItemWrapper = $createCustomListItemNode();
return listItemWrapper.append(node);
}
export function $createCustomListNode(type: ListType): CustomListNode {
return new CustomListNode(type, 1);
}
export function $isCustomListNode(node: LexicalNode | null | undefined): node is CustomListNode {
return node instanceof CustomListNode;
}

View File

@ -1,123 +0,0 @@
import {
DOMConversion,
DOMConversionMap,
DOMConversionOutput,
LexicalNode,
ParagraphNode, SerializedParagraphNode, Spread,
} from "lexical";
import {EditorConfig} from "lexical/LexicalEditor";
import {
CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
SerializedCommonBlockNode,
setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps
} from "./_common";
export type SerializedCustomParagraphNode = Spread<SerializedCommonBlockNode, SerializedParagraphNode>
export class CustomParagraphNode extends ParagraphNode {
__id: string = '';
__alignment: CommonBlockAlignment = '';
__inset: number = 0;
static getType() {
return 'custom-paragraph';
}
setId(id: string) {
const self = this.getWritable();
self.__id = id;
}
getId(): string {
const self = this.getLatest();
return self.__id;
}
setAlignment(alignment: CommonBlockAlignment) {
const self = this.getWritable();
self.__alignment = alignment;
}
getAlignment(): CommonBlockAlignment {
const self = this.getLatest();
return self.__alignment;
}
setInset(size: number) {
const self = this.getWritable();
self.__inset = size;
}
getInset(): number {
const self = this.getLatest();
return self.__inset;
}
static clone(node: CustomParagraphNode): CustomParagraphNode {
const newNode = new CustomParagraphNode(node.__key);
newNode.__id = node.__id;
newNode.__alignment = node.__alignment;
newNode.__inset = node.__inset;
return newNode;
}
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
updateElementWithCommonBlockProps(dom, this);
return dom;
}
updateDOM(prevNode: CustomParagraphNode, dom: HTMLElement, config: EditorConfig): boolean {
return super.updateDOM(prevNode, dom, config)
|| commonPropertiesDifferent(prevNode, this);
}
exportJSON(): SerializedCustomParagraphNode {
return {
...super.exportJSON(),
type: 'custom-paragraph',
version: 1,
id: this.__id,
alignment: this.__alignment,
inset: this.__inset,
};
}
static importJSON(serializedNode: SerializedCustomParagraphNode): CustomParagraphNode {
const node = $createCustomParagraphNode();
deserializeCommonBlockNode(serializedNode, node);
return node;
}
static importDOM(): DOMConversionMap|null {
return {
p(node: HTMLElement): DOMConversion|null {
return {
conversion: (element: HTMLElement): DOMConversionOutput|null => {
const node = $createCustomParagraphNode();
if (element.style.textIndent) {
const indent = parseInt(element.style.textIndent, 10) / 20;
if (indent > 0) {
node.setIndent(indent);
}
}
setCommonBlockPropsFromElement(element, node);
return {node};
},
priority: 1,
};
},
};
}
}
export function $createCustomParagraphNode(): CustomParagraphNode {
return new CustomParagraphNode();
}
export function $isCustomParagraphNode(node: LexicalNode | null | undefined): node is CustomParagraphNode {
return node instanceof CustomParagraphNode;
}

View File

@ -1,115 +0,0 @@
import {
DOMConversionMap,
DOMConversionOutput,
LexicalNode,
Spread
} from "lexical";
import {EditorConfig} from "lexical/LexicalEditor";
import {QuoteNode, SerializedQuoteNode} from "@lexical/rich-text";
import {
CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
SerializedCommonBlockNode,
setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps
} from "./_common";
export type SerializedCustomQuoteNode = Spread<SerializedCommonBlockNode, SerializedQuoteNode>
export class CustomQuoteNode extends QuoteNode {
__id: string = '';
__alignment: CommonBlockAlignment = '';
__inset: number = 0;
static getType() {
return 'custom-quote';
}
setId(id: string) {
const self = this.getWritable();
self.__id = id;
}
getId(): string {
const self = this.getLatest();
return self.__id;
}
setAlignment(alignment: CommonBlockAlignment) {
const self = this.getWritable();
self.__alignment = alignment;
}
getAlignment(): CommonBlockAlignment {
const self = this.getLatest();
return self.__alignment;
}
setInset(size: number) {
const self = this.getWritable();
self.__inset = size;
}
getInset(): number {
const self = this.getLatest();
return self.__inset;
}
static clone(node: CustomQuoteNode) {
const newNode = new CustomQuoteNode(node.__key);
newNode.__id = node.__id;
newNode.__alignment = node.__alignment;
newNode.__inset = node.__inset;
return newNode;
}
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
updateElementWithCommonBlockProps(dom, this);
return dom;
}
updateDOM(prevNode: CustomQuoteNode): boolean {
return commonPropertiesDifferent(prevNode, this);
}
exportJSON(): SerializedCustomQuoteNode {
return {
...super.exportJSON(),
type: 'custom-quote',
version: 1,
id: this.__id,
alignment: this.__alignment,
inset: this.__inset,
};
}
static importJSON(serializedNode: SerializedCustomQuoteNode): CustomQuoteNode {
const node = $createCustomQuoteNode();
deserializeCommonBlockNode(serializedNode, node);
return node;
}
static importDOM(): DOMConversionMap | null {
return {
blockquote: (node: Node) => ({
conversion: $convertBlockquoteElement,
priority: 0,
}),
};
}
}
function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
const node = $createCustomQuoteNode();
setCommonBlockPropsFromElement(element, node);
return {node};
}
export function $createCustomQuoteNode() {
return new CustomQuoteNode();
}
export function $isCustomQuoteNode(node: LexicalNode | null | undefined): node is CustomQuoteNode {
return node instanceof CustomQuoteNode;
}

View File

@ -1,247 +0,0 @@
import {
$createParagraphNode,
$isElementNode,
$isLineBreakNode,
$isTextNode,
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
EditorConfig,
LexicalEditor,
LexicalNode,
Spread
} from "lexical";
import {
$createTableCellNode,
$isTableCellNode,
SerializedTableCellNode,
TableCellHeaderStates,
TableCellNode
} from "@lexical/table";
import {TableCellHeaderState} from "@lexical/table/LexicalTableCellNode";
import {extractStyleMapFromElement, StyleMap} from "../utils/dom";
import {CommonBlockAlignment, extractAlignmentFromElement} from "./_common";
export type SerializedCustomTableCellNode = Spread<{
styles: Record<string, string>;
alignment: CommonBlockAlignment;
}, SerializedTableCellNode>
export class CustomTableCellNode extends TableCellNode {
__styles: StyleMap = new Map;
__alignment: CommonBlockAlignment = '';
static getType(): string {
return 'custom-table-cell';
}
static clone(node: CustomTableCellNode): CustomTableCellNode {
const cellNode = new CustomTableCellNode(
node.__headerState,
node.__colSpan,
node.__width,
node.__key,
);
cellNode.__rowSpan = node.__rowSpan;
cellNode.__styles = new Map(node.__styles);
cellNode.__alignment = node.__alignment;
return cellNode;
}
clearWidth(): void {
const self = this.getWritable();
self.__width = undefined;
}
getStyles(): StyleMap {
const self = this.getLatest();
return new Map(self.__styles);
}
setStyles(styles: StyleMap): void {
const self = this.getWritable();
self.__styles = new Map(styles);
}
setAlignment(alignment: CommonBlockAlignment) {
const self = this.getWritable();
self.__alignment = alignment;
}
getAlignment(): CommonBlockAlignment {
const self = this.getLatest();
return self.__alignment;
}
updateTag(tag: string): void {
const isHeader = tag.toLowerCase() === 'th';
const state = isHeader ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS;
const self = this.getWritable();
self.__headerState = state;
}
createDOM(config: EditorConfig): HTMLElement {
const element = super.createDOM(config);
for (const [name, value] of this.__styles.entries()) {
element.style.setProperty(name, value);
}
if (this.__alignment) {
element.classList.add('align-' + this.__alignment);
}
return element;
}
updateDOM(prevNode: CustomTableCellNode): boolean {
return super.updateDOM(prevNode)
|| this.__styles !== prevNode.__styles
|| this.__alignment !== prevNode.__alignment;
}
static importDOM(): DOMConversionMap | null {
return {
td: (node: Node) => ({
conversion: $convertCustomTableCellNodeElement,
priority: 0,
}),
th: (node: Node) => ({
conversion: $convertCustomTableCellNodeElement,
priority: 0,
}),
};
}
exportDOM(editor: LexicalEditor): DOMExportOutput {
const element = this.createDOM(editor._config);
return {
element
};
}
static importJSON(serializedNode: SerializedCustomTableCellNode): CustomTableCellNode {
const node = $createCustomTableCellNode(
serializedNode.headerState,
serializedNode.colSpan,
serializedNode.width,
);
node.setStyles(new Map(Object.entries(serializedNode.styles)));
node.setAlignment(serializedNode.alignment);
return node;
}
exportJSON(): SerializedCustomTableCellNode {
return {
...super.exportJSON(),
type: 'custom-table-cell',
styles: Object.fromEntries(this.__styles),
alignment: this.__alignment,
};
}
}
function $convertCustomTableCellNodeElement(domNode: Node): DOMConversionOutput {
const output = $convertTableCellNodeElement(domNode);
if (domNode instanceof HTMLElement && output.node instanceof CustomTableCellNode) {
output.node.setStyles(extractStyleMapFromElement(domNode));
output.node.setAlignment(extractAlignmentFromElement(domNode));
}
return output;
}
/**
* Function taken from:
* https://github.com/facebook/lexical/blob/e1881a6e409e1541c10dd0b5378f3a38c9dc8c9e/packages/lexical-table/src/LexicalTableCellNode.ts#L289
* Copyright (c) Meta Platforms, Inc. and affiliates.
* MIT LICENSE
* Modified since copy.
*/
export function $convertTableCellNodeElement(
domNode: Node,
): DOMConversionOutput {
const domNode_ = domNode as HTMLTableCellElement;
const nodeName = domNode.nodeName.toLowerCase();
let width: number | undefined = undefined;
const PIXEL_VALUE_REG_EXP = /^(\d+(?:\.\d+)?)px$/;
if (PIXEL_VALUE_REG_EXP.test(domNode_.style.width)) {
width = parseFloat(domNode_.style.width);
}
const tableCellNode = $createTableCellNode(
nodeName === 'th'
? TableCellHeaderStates.ROW
: TableCellHeaderStates.NO_STATUS,
domNode_.colSpan,
width,
);
tableCellNode.__rowSpan = domNode_.rowSpan;
const style = domNode_.style;
const textDecoration = style.textDecoration.split(' ');
const hasBoldFontWeight =
style.fontWeight === '700' || style.fontWeight === 'bold';
const hasLinethroughTextDecoration = textDecoration.includes('line-through');
const hasItalicFontStyle = style.fontStyle === 'italic';
const hasUnderlineTextDecoration = textDecoration.includes('underline');
return {
after: (childLexicalNodes) => {
if (childLexicalNodes.length === 0) {
childLexicalNodes.push($createParagraphNode());
}
return childLexicalNodes;
},
forChild: (lexicalNode, parentLexicalNode) => {
if ($isTableCellNode(parentLexicalNode) && !$isElementNode(lexicalNode)) {
const paragraphNode = $createParagraphNode();
if (
$isLineBreakNode(lexicalNode) &&
lexicalNode.getTextContent() === '\n'
) {
return null;
}
if ($isTextNode(lexicalNode)) {
if (hasBoldFontWeight) {
lexicalNode.toggleFormat('bold');
}
if (hasLinethroughTextDecoration) {
lexicalNode.toggleFormat('strikethrough');
}
if (hasItalicFontStyle) {
lexicalNode.toggleFormat('italic');
}
if (hasUnderlineTextDecoration) {
lexicalNode.toggleFormat('underline');
}
}
paragraphNode.append(lexicalNode);
return paragraphNode;
}
return lexicalNode;
},
node: tableCellNode,
};
}
export function $createCustomTableCellNode(
headerState: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,
colSpan = 1,
width?: number,
): CustomTableCellNode {
return new CustomTableCellNode(headerState, colSpan, width);
}
export function $isCustomTableCellNode(node: LexicalNode | null | undefined): node is CustomTableCellNode {
return node instanceof CustomTableCellNode;
}

View File

@ -1,106 +0,0 @@
import {
DOMConversionMap,
DOMConversionOutput,
EditorConfig,
LexicalNode,
Spread
} from "lexical";
import {
SerializedTableRowNode,
TableRowNode
} from "@lexical/table";
import {NodeKey} from "lexical/LexicalNode";
import {extractStyleMapFromElement, StyleMap} from "../utils/dom";
export type SerializedCustomTableRowNode = Spread<{
styles: Record<string, string>,
}, SerializedTableRowNode>
export class CustomTableRowNode extends TableRowNode {
__styles: StyleMap = new Map();
constructor(key?: NodeKey) {
super(0, key);
}
static getType(): string {
return 'custom-table-row';
}
static clone(node: CustomTableRowNode): CustomTableRowNode {
const cellNode = new CustomTableRowNode(node.__key);
cellNode.__styles = new Map(node.__styles);
return cellNode;
}
getStyles(): StyleMap {
const self = this.getLatest();
return new Map(self.__styles);
}
setStyles(styles: StyleMap): void {
const self = this.getWritable();
self.__styles = new Map(styles);
}
createDOM(config: EditorConfig): HTMLElement {
const element = super.createDOM(config);
for (const [name, value] of this.__styles.entries()) {
element.style.setProperty(name, value);
}
return element;
}
updateDOM(prevNode: CustomTableRowNode): boolean {
return super.updateDOM(prevNode)
|| this.__styles !== prevNode.__styles;
}
static importDOM(): DOMConversionMap | null {
return {
tr: (node: Node) => ({
conversion: $convertTableRowElement,
priority: 0,
}),
};
}
static importJSON(serializedNode: SerializedCustomTableRowNode): CustomTableRowNode {
const node = $createCustomTableRowNode();
node.setStyles(new Map(Object.entries(serializedNode.styles)));
return node;
}
exportJSON(): SerializedCustomTableRowNode {
return {
...super.exportJSON(),
height: 0,
type: 'custom-table-row',
styles: Object.fromEntries(this.__styles),
};
}
}
export function $convertTableRowElement(domNode: Node): DOMConversionOutput {
const rowNode = $createCustomTableRowNode();
if (domNode instanceof HTMLElement) {
rowNode.setStyles(extractStyleMapFromElement(domNode));
}
return {node: rowNode};
}
export function $createCustomTableRowNode(): CustomTableRowNode {
return new CustomTableRowNode();
}
export function $isCustomTableRowNode(node: LexicalNode | null | undefined): node is CustomTableRowNode {
return node instanceof CustomTableRowNode;
}

View File

@ -1,166 +0,0 @@
import {SerializedTableNode, TableNode} from "@lexical/table";
import {DOMConversion, DOMConversionMap, DOMConversionOutput, LexicalNode, Spread} from "lexical";
import {EditorConfig} from "lexical/LexicalEditor";
import {el, extractStyleMapFromElement, StyleMap} from "../utils/dom";
import {getTableColumnWidths} from "../utils/tables";
import {
CommonBlockAlignment, deserializeCommonBlockNode,
SerializedCommonBlockNode,
setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps
} from "./_common";
export type SerializedCustomTableNode = Spread<Spread<{
colWidths: string[];
styles: Record<string, string>,
}, SerializedTableNode>, SerializedCommonBlockNode>
export class CustomTableNode extends TableNode {
__id: string = '';
__colWidths: string[] = [];
__styles: StyleMap = new Map;
__alignment: CommonBlockAlignment = '';
__inset: number = 0;
static getType() {
return 'custom-table';
}
setId(id: string) {
const self = this.getWritable();
self.__id = id;
}
getId(): string {
const self = this.getLatest();
return self.__id;
}
setAlignment(alignment: CommonBlockAlignment) {
const self = this.getWritable();
self.__alignment = alignment;
}
getAlignment(): CommonBlockAlignment {
const self = this.getLatest();
return self.__alignment;
}
setInset(size: number) {
const self = this.getWritable();
self.__inset = size;
}
getInset(): number {
const self = this.getLatest();
return self.__inset;
}
setColWidths(widths: string[]) {
const self = this.getWritable();
self.__colWidths = widths;
}
getColWidths(): string[] {
const self = this.getLatest();
return self.__colWidths;
}
getStyles(): StyleMap {
const self = this.getLatest();
return new Map(self.__styles);
}
setStyles(styles: StyleMap): void {
const self = this.getWritable();
self.__styles = new Map(styles);
}
static clone(node: CustomTableNode) {
const newNode = new CustomTableNode(node.__key);
newNode.__id = node.__id;
newNode.__colWidths = node.__colWidths;
newNode.__styles = new Map(node.__styles);
newNode.__alignment = node.__alignment;
newNode.__inset = node.__inset;
return newNode;
}
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
updateElementWithCommonBlockProps(dom, this);
const colWidths = this.getColWidths();
if (colWidths.length > 0) {
const colgroup = el('colgroup');
for (const width of colWidths) {
const col = el('col');
if (width) {
col.style.width = width;
}
colgroup.append(col);
}
dom.append(colgroup);
}
for (const [name, value] of this.__styles.entries()) {
dom.style.setProperty(name, value);
}
return dom;
}
updateDOM(): boolean {
return true;
}
exportJSON(): SerializedCustomTableNode {
return {
...super.exportJSON(),
type: 'custom-table',
version: 1,
id: this.__id,
colWidths: this.__colWidths,
styles: Object.fromEntries(this.__styles),
alignment: this.__alignment,
inset: this.__inset,
};
}
static importJSON(serializedNode: SerializedCustomTableNode): CustomTableNode {
const node = $createCustomTableNode();
deserializeCommonBlockNode(serializedNode, node);
node.setColWidths(serializedNode.colWidths);
node.setStyles(new Map(Object.entries(serializedNode.styles)));
return node;
}
static importDOM(): DOMConversionMap|null {
return {
table(node: HTMLElement): DOMConversion|null {
return {
conversion: (element: HTMLElement): DOMConversionOutput|null => {
const node = $createCustomTableNode();
setCommonBlockPropsFromElement(element, node);
const colWidths = getTableColumnWidths(element as HTMLTableElement);
node.setColWidths(colWidths);
node.setStyles(extractStyleMapFromElement(element));
return {node};
},
priority: 1,
};
},
};
}
}
export function $createCustomTableNode(): CustomTableNode {
return new CustomTableNode();
}
export function $isCustomTableNode(node: LexicalNode | null | undefined): node is CustomTableNode {
return node instanceof CustomTableNode;
}

View File

@ -1,128 +0,0 @@
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {CalloutNode} from './callout';
import {
ElementNode,
KlassConstructor,
LexicalNode,
LexicalNodeReplacement, NodeMutation,
ParagraphNode
} from "lexical";
import {CustomParagraphNode} from "./custom-paragraph";
import {LinkNode} from "@lexical/link";
import {ImageNode} from "./image";
import {DetailsNode, SummaryNode} from "./details";
import {ListItemNode, ListNode} from "@lexical/list";
import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
import {CustomTableNode} from "./custom-table";
import {HorizontalRuleNode} from "./horizontal-rule";
import {CodeBlockNode} from "./code-block";
import {DiagramNode} from "./diagram";
import {EditorUiContext} from "../ui/framework/core";
import {MediaNode} from "./media";
import {CustomListItemNode} from "./custom-list-item";
import {CustomTableCellNode} from "./custom-table-cell";
import {CustomTableRowNode} from "./custom-table-row";
import {CustomHeadingNode} from "./custom-heading";
import {CustomQuoteNode} from "./custom-quote";
import {CustomListNode} from "./custom-list";
/**
* Load the nodes for lexical.
*/
export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {
return [
CalloutNode,
CustomHeadingNode,
CustomQuoteNode,
CustomListNode,
CustomListItemNode, // TODO - Alignment?
CustomTableNode,
CustomTableRowNode,
CustomTableCellNode,
ImageNode, // TODO - Alignment
HorizontalRuleNode,
DetailsNode, SummaryNode,
CodeBlockNode,
DiagramNode,
MediaNode, // TODO - Alignment
CustomParagraphNode,
LinkNode,
{
replace: ParagraphNode,
with: (node: ParagraphNode) => {
return new CustomParagraphNode();
}
},
{
replace: HeadingNode,
with: (node: HeadingNode) => {
return new CustomHeadingNode(node.__tag);
}
},
{
replace: QuoteNode,
with: (node: QuoteNode) => {
return new CustomQuoteNode();
}
},
{
replace: ListNode,
with: (node: ListNode) => {
return new CustomListNode(node.getListType(), node.getStart());
}
},
{
replace: ListItemNode,
with: (node: ListItemNode) => {
return new CustomListItemNode(node.__value, node.__checked);
}
},
{
replace: TableNode,
with(node: TableNode) {
return new CustomTableNode();
}
},
{
replace: TableRowNode,
with(node: TableRowNode) {
return new CustomTableRowNode();
}
},
{
replace: TableCellNode,
with: (node: TableCellNode) => {
const cell = new CustomTableCellNode(
node.__headerState,
node.__colSpan,
node.__width,
);
cell.__rowSpan = node.__rowSpan;
return cell;
}
},
];
}
export function registerCommonNodeMutationListeners(context: EditorUiContext): void {
const decorated = [ImageNode, CodeBlockNode, DiagramNode];
const decorationDestroyListener = (mutations: Map<string, NodeMutation>): void => {
for (let [nodeKey, mutation] of mutations) {
if (mutation === "destroyed") {
const decorator = context.manager.getDecoratorByNodeKey(nodeKey);
if (decorator) {
decorator.destroy(context);
}
}
}
};
for (let decoratedNode of decorated) {
// Have to pass a unique function here since they are stored by lexical keyed on listener function.
context.editor.registerMutationListener(decoratedNode, (mutations) => decorationDestroyListener(mutations));
}
}
export type LexicalNodeMatcher = (node: LexicalNode|null|undefined) => boolean;
export type LexicalElementNodeCreator = () => ElementNode;

View File

@ -1,4 +1,5 @@
import { import {
$createParagraphNode,
$insertNodes, $insertNodes,
$isDecoratorNode, COMMAND_PRIORITY_HIGH, DROP_COMMAND, $isDecoratorNode, COMMAND_PRIORITY_HIGH, DROP_COMMAND,
LexicalEditor, LexicalEditor,
@ -7,8 +8,7 @@ import {
import {$insertNewBlockNodesAtSelection, $selectSingleNode} from "../utils/selection"; import {$insertNewBlockNodesAtSelection, $selectSingleNode} from "../utils/selection";
import {$getNearestBlockNodeForCoords, $htmlToBlockNodes} from "../utils/nodes"; import {$getNearestBlockNodeForCoords, $htmlToBlockNodes} from "../utils/nodes";
import {Clipboard} from "../../services/clipboard"; import {Clipboard} from "../../services/clipboard";
import {$createImageNode} from "../nodes/image"; import {$createImageNode} from "@lexical/rich-text/LexicalImageNode";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
import {$createLinkNode} from "@lexical/link"; import {$createLinkNode} from "@lexical/link";
import {EditorImageData, uploadImageFile} from "../utils/images"; import {EditorImageData, uploadImageFile} from "../utils/images";
import {EditorUiContext} from "../ui/framework/core"; import {EditorUiContext} from "../ui/framework/core";
@ -67,7 +67,7 @@ function handleMediaInsert(data: DataTransfer, context: EditorUiContext): boolea
for (const imageFile of images) { for (const imageFile of images) {
const loadingImage = window.baseUrl('/loading.gif'); const loadingImage = window.baseUrl('/loading.gif');
const loadingNode = $createImageNode(loadingImage); const loadingNode = $createImageNode(loadingImage);
const imageWrap = $createCustomParagraphNode(); const imageWrap = $createParagraphNode();
imageWrap.append(loadingNode); imageWrap.append(loadingNode);
$insertNodes([imageWrap]); $insertNodes([imageWrap]);

View File

@ -1,5 +1,6 @@
import {EditorUiContext} from "../ui/framework/core"; import {EditorUiContext} from "../ui/framework/core";
import { import {
$createParagraphNode,
$getSelection, $getSelection,
$isDecoratorNode, $isDecoratorNode,
COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_LOW,
@ -9,13 +10,12 @@ import {
LexicalEditor, LexicalEditor,
LexicalNode LexicalNode
} from "lexical"; } from "lexical";
import {$isImageNode} from "../nodes/image"; import {$isImageNode} from "@lexical/rich-text/LexicalImageNode";
import {$isMediaNode} from "../nodes/media"; import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode";
import {getLastSelection} from "../utils/selection"; import {getLastSelection} from "../utils/selection";
import {$getNearestNodeBlockParent} from "../utils/nodes"; import {$getNearestNodeBlockParent} from "../utils/nodes";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
import {$isCustomListItemNode} from "../nodes/custom-list-item";
import {$setInsetForSelection} from "../utils/lists"; import {$setInsetForSelection} from "../utils/lists";
import {$isListItemNode} from "@lexical/list";
function isSingleSelectedNode(nodes: LexicalNode[]): boolean { function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
if (nodes.length === 1) { if (nodes.length === 1) {
@ -45,7 +45,7 @@ function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEve
if (nearestBlock) { if (nearestBlock) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
editor.update(() => { editor.update(() => {
const newParagraph = $createCustomParagraphNode(); const newParagraph = $createParagraphNode();
nearestBlock.insertAfter(newParagraph); nearestBlock.insertAfter(newParagraph);
newParagraph.select(); newParagraph.select();
}); });
@ -62,7 +62,7 @@ function handleInsetOnTab(editor: LexicalEditor, event: KeyboardEvent|null): boo
const change = event?.shiftKey ? -40 : 40; const change = event?.shiftKey ? -40 : 40;
const selection = $getSelection(); const selection = $getSelection();
const nodes = selection?.getNodes() || []; const nodes = selection?.getNodes() || [];
if (nodes.length > 1 || (nodes.length === 1 && $isCustomListItemNode(nodes[0].getParent()))) { if (nodes.length > 1 || (nodes.length === 1 && $isListItemNode(nodes[0].getParent()))) {
editor.update(() => { editor.update(() => {
$setInsetForSelection(editor, change); $setInsetForSelection(editor, change);
}); });

View File

@ -6,12 +6,12 @@ import {
toggleSelectionAsHeading, toggleSelectionAsList, toggleSelectionAsHeading, toggleSelectionAsList,
toggleSelectionAsParagraph toggleSelectionAsParagraph
} from "../utils/formats"; } from "../utils/formats";
import {HeadingTagType} from "@lexical/rich-text";
import {EditorUiContext} from "../ui/framework/core"; import {EditorUiContext} from "../ui/framework/core";
import {$getNodeFromSelection} from "../utils/selection"; import {$getNodeFromSelection} from "../utils/selection";
import {$isLinkNode, LinkNode} from "@lexical/link"; import {$isLinkNode, LinkNode} from "@lexical/link";
import {$showLinkForm} from "../ui/defaults/forms/objects"; import {$showLinkForm} from "../ui/defaults/forms/objects";
import {showLinkSelector} from "../utils/links"; import {showLinkSelector} from "../utils/links";
import {HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode";
function headerHandler(editor: LexicalEditor, tag: HeadingTagType): boolean { function headerHandler(editor: LexicalEditor, tag: HeadingTagType): boolean {
toggleSelectionAsHeading(editor, tag); toggleSelectionAsHeading(editor, tag);

View File

@ -2,7 +2,11 @@
## In progress ## In progress
// Reorg
- Merge custom nodes into original nodes
- Reduce down to use CommonBlockNode where possible
- Remove existing formatType/ElementFormatType references (replaced with alignment).
- Remove existing indent references (replaced with inset).
## Main Todo ## Main Todo

View File

@ -1,7 +1,7 @@
import {EditorDecorator} from "../framework/decorator"; import {EditorDecorator} from "../framework/decorator";
import {EditorUiContext} from "../framework/core"; import {EditorUiContext} from "../framework/core";
import {$openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block"; import {$openCodeEditorForNode, CodeBlockNode} from "@lexical/rich-text/LexicalCodeBlockNode";
import {$isDecoratorNode, BaseSelection} from "lexical"; import {BaseSelection} from "lexical";
import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection"; import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection";

View File

@ -1,7 +1,7 @@
import {EditorDecorator} from "../framework/decorator"; import {EditorDecorator} from "../framework/decorator";
import {EditorUiContext} from "../framework/core"; import {EditorUiContext} from "../framework/core";
import {BaseSelection} from "lexical"; import {BaseSelection} from "lexical";
import {DiagramNode} from "../../nodes/diagram"; import {DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection"; import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection";
import {$openDrawingEditorForNode} from "../../utils/diagrams"; import {$openDrawingEditorForNode} from "../../utils/diagrams";

View File

@ -9,9 +9,9 @@ import ltrIcon from "@icons/editor/direction-ltr.svg";
import rtlIcon from "@icons/editor/direction-rtl.svg"; import rtlIcon from "@icons/editor/direction-rtl.svg";
import { import {
$getBlockElementNodesInSelection, $getBlockElementNodesInSelection,
$selectionContainsAlignment, $selectionContainsDirection, $selectSingleNode, $toggleSelection, getLastSelection $selectionContainsAlignment, $selectionContainsDirection, $selectSingleNode, getLastSelection
} from "../../../utils/selection"; } from "../../../utils/selection";
import {CommonBlockAlignment} from "../../../nodes/_common"; import {CommonBlockAlignment} from "lexical/nodes/common";
import {nodeHasAlignment} from "../../../utils/nodes"; import {nodeHasAlignment} from "../../../utils/nodes";

View File

@ -1,19 +1,15 @@
import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../../nodes/callout"; import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "@lexical/rich-text/LexicalCalloutNode";
import {EditorButtonDefinition} from "../../framework/buttons"; import {EditorButtonDefinition} from "../../framework/buttons";
import {EditorUiContext} from "../../framework/core"; import {EditorUiContext} from "../../framework/core";
import {$isParagraphNode, BaseSelection, LexicalNode} from "lexical"; import {$isParagraphNode, BaseSelection, LexicalNode} from "lexical";
import {
$isHeadingNode,
$isQuoteNode,
HeadingNode,
HeadingTagType
} from "@lexical/rich-text";
import {$selectionContainsNodeType, $toggleSelectionBlockNodeType} from "../../../utils/selection"; import {$selectionContainsNodeType, $toggleSelectionBlockNodeType} from "../../../utils/selection";
import { import {
toggleSelectionAsBlockquote, toggleSelectionAsBlockquote,
toggleSelectionAsHeading, toggleSelectionAsHeading,
toggleSelectionAsParagraph toggleSelectionAsParagraph
} from "../../../utils/formats"; } from "../../../utils/formats";
import {$isHeadingNode, HeadingNode, HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode";
import {$isQuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition { function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
return { return {

View File

@ -2,27 +2,26 @@ import {EditorButtonDefinition} from "../../framework/buttons";
import linkIcon from "@icons/editor/link.svg"; import linkIcon from "@icons/editor/link.svg";
import {EditorUiContext} from "../../framework/core"; import {EditorUiContext} from "../../framework/core";
import { import {
$createTextNode,
$getRoot, $getRoot,
$getSelection, $insertNodes, $getSelection, $insertNodes,
BaseSelection, BaseSelection,
ElementNode, isCurrentlyReadOnlyMode ElementNode
} from "lexical"; } from "lexical";
import {$isLinkNode, LinkNode} from "@lexical/link"; import {$isLinkNode, LinkNode} from "@lexical/link";
import unlinkIcon from "@icons/editor/unlink.svg"; import unlinkIcon from "@icons/editor/unlink.svg";
import imageIcon from "@icons/editor/image.svg"; import imageIcon from "@icons/editor/image.svg";
import {$isImageNode, ImageNode} from "../../../nodes/image"; import {$isImageNode, ImageNode} from "@lexical/rich-text/LexicalImageNode";
import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg"; import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg";
import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../../nodes/horizontal-rule"; import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "@lexical/rich-text/LexicalHorizontalRuleNode";
import codeBlockIcon from "@icons/editor/code-block.svg"; import codeBlockIcon from "@icons/editor/code-block.svg";
import {$isCodeBlockNode} from "../../../nodes/code-block"; import {$isCodeBlockNode} from "@lexical/rich-text/LexicalCodeBlockNode";
import editIcon from "@icons/edit.svg"; import editIcon from "@icons/edit.svg";
import diagramIcon from "@icons/editor/diagram.svg"; import diagramIcon from "@icons/editor/diagram.svg";
import {$createDiagramNode, DiagramNode} from "../../../nodes/diagram"; import {$createDiagramNode, DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
import detailsIcon from "@icons/editor/details.svg"; import detailsIcon from "@icons/editor/details.svg";
import mediaIcon from "@icons/editor/media.svg"; import mediaIcon from "@icons/editor/media.svg";
import {$createDetailsNode, $isDetailsNode} from "../../../nodes/details"; import {$createDetailsNode, $isDetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
import {$isMediaNode, MediaNode} from "../../../nodes/media"; import {$isMediaNode, MediaNode} from "@lexical/rich-text/LexicalMediaNode";
import { import {
$getNodeFromSelection, $getNodeFromSelection,
$insertNewBlockNodeAtSelection, $insertNewBlockNodeAtSelection,

View File

@ -9,17 +9,15 @@ import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg";
import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg"; import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
import {EditorUiContext} from "../../framework/core"; import {EditorUiContext} from "../../framework/core";
import {$getSelection, BaseSelection} from "lexical"; import {$getSelection, BaseSelection} from "lexical";
import {$isCustomTableNode} from "../../../nodes/custom-table";
import { import {
$deleteTableColumn__EXPERIMENTAL, $deleteTableColumn__EXPERIMENTAL,
$deleteTableRow__EXPERIMENTAL, $deleteTableRow__EXPERIMENTAL,
$insertTableColumn__EXPERIMENTAL, $insertTableColumn__EXPERIMENTAL,
$insertTableRow__EXPERIMENTAL, $insertTableRow__EXPERIMENTAL, $isTableCellNode,
$isTableNode, $isTableSelection, $unmergeCell, TableCellNode, $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode,
} from "@lexical/table"; } from "@lexical/table";
import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/selection"; import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/selection";
import {$getParentOfType} from "../../../utils/nodes"; import {$getParentOfType} from "../../../utils/nodes";
import {$isCustomTableCellNode} from "../../../nodes/custom-table-cell";
import {$showCellPropertiesForm, $showRowPropertiesForm, $showTablePropertiesForm} from "../forms/tables"; import {$showCellPropertiesForm, $showRowPropertiesForm, $showTablePropertiesForm} from "../forms/tables";
import { import {
$clearTableFormatting, $clearTableFormatting,
@ -27,7 +25,6 @@ import {
$getTableRowsFromSelection, $getTableRowsFromSelection,
$mergeTableCellsInSelection $mergeTableCellsInSelection
} from "../../../utils/tables"; } from "../../../utils/tables";
import {$isCustomTableRowNode} from "../../../nodes/custom-table-row";
import { import {
$copySelectedColumnsToClipboard, $copySelectedColumnsToClipboard,
$copySelectedRowsToClipboard, $copySelectedRowsToClipboard,
@ -41,7 +38,7 @@ import {
} from "../../../utils/table-copy-paste"; } from "../../../utils/table-copy-paste";
const neverActive = (): boolean => false; const neverActive = (): boolean => false;
const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode); const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isTableCellNode);
export const table: EditorBasicButtonDefinition = { export const table: EditorBasicButtonDefinition = {
label: 'Table', label: 'Table',
@ -54,7 +51,7 @@ export const tableProperties: EditorButtonDefinition = {
action(context: EditorUiContext) { action(context: EditorUiContext) {
context.editor.getEditorState().read(() => { context.editor.getEditorState().read(() => {
const table = $getTableFromSelection($getSelection()); const table = $getTableFromSelection($getSelection());
if ($isCustomTableNode(table)) { if ($isTableNode(table)) {
$showTablePropertiesForm(table, context); $showTablePropertiesForm(table, context);
} }
}); });
@ -68,13 +65,13 @@ export const clearTableFormatting: EditorButtonDefinition = {
format: 'long', format: 'long',
action(context: EditorUiContext) { action(context: EditorUiContext) {
context.editor.update(() => { context.editor.update(() => {
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode); const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
if (!$isCustomTableCellNode(cell)) { if (!$isTableCellNode(cell)) {
return; return;
} }
const table = $getParentOfType(cell, $isTableNode); const table = $getParentOfType(cell, $isTableNode);
if ($isCustomTableNode(table)) { if ($isTableNode(table)) {
$clearTableFormatting(table); $clearTableFormatting(table);
} }
}); });
@ -88,13 +85,13 @@ export const resizeTableToContents: EditorButtonDefinition = {
format: 'long', format: 'long',
action(context: EditorUiContext) { action(context: EditorUiContext) {
context.editor.update(() => { context.editor.update(() => {
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode); const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
if (!$isCustomTableCellNode(cell)) { if (!$isTableCellNode(cell)) {
return; return;
} }
const table = $getParentOfType(cell, $isCustomTableNode); const table = $getParentOfType(cell, $isTableNode);
if ($isCustomTableNode(table)) { if ($isTableNode(table)) {
$clearTableSizes(table); $clearTableSizes(table);
} }
}); });
@ -108,7 +105,7 @@ export const deleteTable: EditorButtonDefinition = {
icon: deleteIcon, icon: deleteIcon,
action(context: EditorUiContext) { action(context: EditorUiContext) {
context.editor.update(() => { context.editor.update(() => {
const table = $getNodeFromSelection($getSelection(), $isCustomTableNode); const table = $getNodeFromSelection($getSelection(), $isTableNode);
if (table) { if (table) {
table.remove(); table.remove();
} }
@ -169,7 +166,7 @@ export const rowProperties: EditorButtonDefinition = {
action(context: EditorUiContext) { action(context: EditorUiContext) {
context.editor.getEditorState().read(() => { context.editor.getEditorState().read(() => {
const rows = $getTableRowsFromSelection($getSelection()); const rows = $getTableRowsFromSelection($getSelection());
if ($isCustomTableRowNode(rows[0])) { if ($isTableRowNode(rows[0])) {
$showRowPropertiesForm(rows[0], context); $showRowPropertiesForm(rows[0], context);
} }
}); });
@ -350,8 +347,8 @@ export const cellProperties: EditorButtonDefinition = {
format: 'long', format: 'long',
action(context: EditorUiContext) { action(context: EditorUiContext) {
context.editor.getEditorState().read(() => { context.editor.getEditorState().read(() => {
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode); const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
if ($isCustomTableCellNode(cell)) { if ($isTableCellNode(cell)) {
$showCellPropertiesForm(cell, context); $showCellPropertiesForm(cell, context);
} }
}); });
@ -387,7 +384,7 @@ export const splitCell: EditorButtonDefinition = {
}, },
isActive: neverActive, isActive: neverActive,
isDisabled(selection) { isDisabled(selection) {
const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as TableCellNode|null; const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode|null;
if (cell) { if (cell) {
const merged = cell.getRowSpan() > 1 || cell.getColSpan() > 1; const merged = cell.getRowSpan() > 1 || cell.getColSpan() > 1;
return !merged; return !merged;

View File

@ -5,11 +5,10 @@ import {
EditorSelectFormFieldDefinition EditorSelectFormFieldDefinition
} from "../../framework/forms"; } from "../../framework/forms";
import {EditorUiContext} from "../../framework/core"; import {EditorUiContext} from "../../framework/core";
import {$createNodeSelection, $createTextNode, $getSelection, $insertNodes, $setSelection} from "lexical"; import {$createNodeSelection, $getSelection, $insertNodes, $setSelection} from "lexical";
import {$isImageNode, ImageNode} from "../../../nodes/image"; import {$isImageNode, ImageNode} from "@lexical/rich-text/LexicalImageNode";
import {$createLinkNode, $isLinkNode, LinkNode} from "@lexical/link"; import {LinkNode} from "@lexical/link";
import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "../../../nodes/media"; import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "@lexical/rich-text/LexicalMediaNode";
import {$insertNodeToNearestRoot} from "@lexical/utils";
import {$getNodeFromSelection, getLastSelection} from "../../../utils/selection"; import {$getNodeFromSelection, getLastSelection} from "../../../utils/selection";
import {EditorFormModal} from "../../framework/modals"; import {EditorFormModal} from "../../framework/modals";
import {EditorActionField} from "../../framework/blocks/action-field"; import {EditorActionField} from "../../framework/blocks/action-field";

View File

@ -5,9 +5,8 @@ import {
EditorSelectFormFieldDefinition EditorSelectFormFieldDefinition
} from "../../framework/forms"; } from "../../framework/forms";
import {EditorUiContext} from "../../framework/core"; import {EditorUiContext} from "../../framework/core";
import {CustomTableCellNode} from "../../../nodes/custom-table-cell";
import {EditorFormModal} from "../../framework/modals"; import {EditorFormModal} from "../../framework/modals";
import {$getSelection, ElementFormatType} from "lexical"; import {$getSelection} from "lexical";
import { import {
$forEachTableCell, $getCellPaddingForTable, $forEachTableCell, $getCellPaddingForTable,
$getTableCellColumnWidth, $getTableCellColumnWidth,
@ -16,8 +15,8 @@ import {
$setTableCellColumnWidth $setTableCellColumnWidth
} from "../../../utils/tables"; } from "../../../utils/tables";
import {formatSizeValue} from "../../../utils/dom"; import {formatSizeValue} from "../../../utils/dom";
import {CustomTableRowNode} from "../../../nodes/custom-table-row"; import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
import {CustomTableNode} from "../../../nodes/custom-table"; import {CommonBlockAlignment} from "lexical/nodes/common";
const borderStyleInput: EditorSelectFormFieldDefinition = { const borderStyleInput: EditorSelectFormFieldDefinition = {
label: 'Border style', label: 'Border style',
@ -62,14 +61,14 @@ const alignmentInput: EditorSelectFormFieldDefinition = {
} }
}; };
export function $showCellPropertiesForm(cell: CustomTableCellNode, context: EditorUiContext): EditorFormModal { export function $showCellPropertiesForm(cell: TableCellNode, context: EditorUiContext): EditorFormModal {
const styles = cell.getStyles(); const styles = cell.getStyles();
const modalForm = context.manager.createModal('cell_properties'); const modalForm = context.manager.createModal('cell_properties');
modalForm.show({ modalForm.show({
width: $getTableCellColumnWidth(context.editor, cell), width: $getTableCellColumnWidth(context.editor, cell),
height: styles.get('height') || '', height: styles.get('height') || '',
type: cell.getTag(), type: cell.getTag(),
h_align: cell.getFormatType(), h_align: cell.getAlignment(),
v_align: styles.get('vertical-align') || '', v_align: styles.get('vertical-align') || '',
border_width: styles.get('border-width') || '', border_width: styles.get('border-width') || '',
border_style: styles.get('border-style') || '', border_style: styles.get('border-style') || '',
@ -89,7 +88,7 @@ export const cellProperties: EditorFormDefinition = {
$setTableCellColumnWidth(cell, width); $setTableCellColumnWidth(cell, width);
cell.updateTag(formData.get('type')?.toString() || ''); cell.updateTag(formData.get('type')?.toString() || '');
cell.setFormat((formData.get('h_align')?.toString() || '') as ElementFormatType); cell.setAlignment((formData.get('h_align')?.toString() || '') as CommonBlockAlignment);
const styles = cell.getStyles(); const styles = cell.getStyles();
styles.set('height', formatSizeValue(formData.get('height')?.toString() || '')); styles.set('height', formatSizeValue(formData.get('height')?.toString() || ''));
@ -172,7 +171,7 @@ export const cellProperties: EditorFormDefinition = {
], ],
}; };
export function $showRowPropertiesForm(row: CustomTableRowNode, context: EditorUiContext): EditorFormModal { export function $showRowPropertiesForm(row: TableRowNode, context: EditorUiContext): EditorFormModal {
const styles = row.getStyles(); const styles = row.getStyles();
const modalForm = context.manager.createModal('row_properties'); const modalForm = context.manager.createModal('row_properties');
modalForm.show({ modalForm.show({
@ -216,7 +215,7 @@ export const rowProperties: EditorFormDefinition = {
], ],
}; };
export function $showTablePropertiesForm(table: CustomTableNode, context: EditorUiContext): EditorFormModal { export function $showTablePropertiesForm(table: TableNode, context: EditorUiContext): EditorFormModal {
const styles = table.getStyles(); const styles = table.getStyles();
const modalForm = context.manager.createModal('table_properties'); const modalForm = context.manager.createModal('table_properties');
modalForm.show({ modalForm.show({
@ -229,7 +228,7 @@ export function $showTablePropertiesForm(table: CustomTableNode, context: Editor
border_color: styles.get('border-color') || '', border_color: styles.get('border-color') || '',
background_color: styles.get('background-color') || '', background_color: styles.get('background-color') || '',
// caption: '', TODO // caption: '', TODO
align: table.getFormatType(), align: table.getAlignment(),
}); });
return modalForm; return modalForm;
} }
@ -253,12 +252,12 @@ export const tableProperties: EditorFormDefinition = {
styles.set('background-color', formData.get('background_color')?.toString() || ''); styles.set('background-color', formData.get('background_color')?.toString() || '');
table.setStyles(styles); table.setStyles(styles);
table.setFormat(formData.get('align') as ElementFormatType); table.setAlignment(formData.get('align') as CommonBlockAlignment);
const cellPadding = (formData.get('cell_padding')?.toString() || ''); const cellPadding = (formData.get('cell_padding')?.toString() || '');
if (cellPadding) { if (cellPadding) {
const cellPaddingFormatted = formatSizeValue(cellPadding); const cellPaddingFormatted = formatSizeValue(cellPadding);
$forEachTableCell(table, (cell: CustomTableCellNode) => { $forEachTableCell(table, (cell: TableCellNode) => {
const styles = cell.getStyles(); const styles = cell.getStyles();
styles.set('padding', cellPaddingFormatted); styles.set('padding', cellPaddingFormatted);
cell.setStyles(styles); cell.setStyles(styles);

View File

@ -1,14 +1,13 @@
import {EditorContainerUiElement} from "../core"; import {EditorContainerUiElement} from "../core";
import {el} from "../../../utils/dom"; import {el} from "../../../utils/dom";
import {EditorFormField} from "../forms"; import {EditorFormField} from "../forms";
import {CustomHeadingNode} from "../../../nodes/custom-heading";
import {$getAllNodesOfType} from "../../../utils/nodes"; import {$getAllNodesOfType} from "../../../utils/nodes";
import {$isHeadingNode} from "@lexical/rich-text";
import {uniqueIdSmall} from "../../../../services/util"; import {uniqueIdSmall} from "../../../../services/util";
import {$isHeadingNode, HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
export class LinkField extends EditorContainerUiElement { export class LinkField extends EditorContainerUiElement {
protected input: EditorFormField; protected input: EditorFormField;
protected headerMap = new Map<string, CustomHeadingNode>(); protected headerMap = new Map<string, HeadingNode>();
constructor(input: EditorFormField) { constructor(input: EditorFormField) {
super([input]); super([input]);
@ -43,7 +42,7 @@ export class LinkField extends EditorContainerUiElement {
return container; return container;
} }
updateFormFromHeader(header: CustomHeadingNode) { updateFormFromHeader(header: HeadingNode) {
this.getHeaderIdAndText(header).then(({id, text}) => { this.getHeaderIdAndText(header).then(({id, text}) => {
console.log('updating form', id, text); console.log('updating form', id, text);
const modal = this.getContext().manager.getActiveModal('link'); const modal = this.getContext().manager.getActiveModal('link');
@ -57,7 +56,7 @@ export class LinkField extends EditorContainerUiElement {
}); });
} }
getHeaderIdAndText(header: CustomHeadingNode): Promise<{id: string, text: string}> { getHeaderIdAndText(header: HeadingNode): Promise<{id: string, text: string}> {
return new Promise((res) => { return new Promise((res) => {
this.getContext().editor.update(() => { this.getContext().editor.update(() => {
let id = header.getId(); let id = header.getId();
@ -75,7 +74,7 @@ export class LinkField extends EditorContainerUiElement {
updateDataList(listEl: HTMLElement) { updateDataList(listEl: HTMLElement) {
this.getContext().editor.getEditorState().read(() => { this.getContext().editor.getEditorState().read(() => {
const headers = $getAllNodesOfType($isHeadingNode) as CustomHeadingNode[]; const headers = $getAllNodesOfType($isHeadingNode) as HeadingNode[];
this.headerMap.clear(); this.headerMap.clear();
const listEls: HTMLElement[] = []; const listEls: HTMLElement[] = [];

View File

@ -1,6 +1,5 @@
import {EditorUiElement} from "../core"; import {EditorUiElement} from "../core";
import {$createTableNodeWithDimensions} from "@lexical/table"; import {$createTableNodeWithDimensions} from "@lexical/table";
import {CustomTableNode} from "../../../nodes/custom-table";
import {$insertNewBlockNodeAtSelection} from "../../../utils/selection"; import {$insertNewBlockNodeAtSelection} from "../../../utils/selection";
import {el} from "../../../utils/dom"; import {el} from "../../../utils/dom";
@ -78,7 +77,7 @@ export class EditorTableCreator extends EditorUiElement {
const colWidths = Array(columns).fill(targetColWidth + 'px'); const colWidths = Array(columns).fill(targetColWidth + 'px');
this.getContext().editor.update(() => { this.getContext().editor.update(() => {
const table = $createTableNodeWithDimensions(rows, columns, false) as CustomTableNode; const table = $createTableNodeWithDimensions(rows, columns, false);
table.setColWidths(colWidths); table.setColWidths(colWidths);
$insertNewBlockNodeAtSelection(table); $insertNewBlockNodeAtSelection(table);
}); });

View File

@ -1,10 +1,10 @@
import {BaseSelection, LexicalNode,} from "lexical"; import {BaseSelection, LexicalNode,} from "lexical";
import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker"; import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker";
import {el} from "../../../utils/dom"; import {el} from "../../../utils/dom";
import {$isImageNode} from "../../../nodes/image"; import {$isImageNode} from "@lexical/rich-text/LexicalImageNode";
import {EditorUiContext} from "../core"; import {EditorUiContext} from "../core";
import {NodeHasSize} from "../../../nodes/_common"; import {NodeHasSize} from "lexical/nodes/common";
import {$isMediaNode} from "../../../nodes/media"; import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode";
function isNodeWithSize(node: LexicalNode): node is NodeHasSize&LexicalNode { function isNodeWithSize(node: LexicalNode): node is NodeHasSize&LexicalNode {
return $isImageNode(node) || $isMediaNode(node); return $isImageNode(node) || $isMediaNode(node);

View File

@ -1,7 +1,6 @@
import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical"; import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical";
import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker"; import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker";
import {CustomTableNode} from "../../../nodes/custom-table"; import {TableNode, TableRowNode} from "@lexical/table";
import {TableRowNode} from "@lexical/table";
import {el} from "../../../utils/dom"; import {el} from "../../../utils/dom";
import {$getTableColumnWidth, $setTableColumnWidth} from "../../../utils/tables"; import {$getTableColumnWidth, $setTableColumnWidth} from "../../../utils/tables";
@ -148,7 +147,7 @@ class TableResizer {
_this.editor.update(() => { _this.editor.update(() => {
const table = $getNearestNodeFromDOMNode(parentTable); const table = $getNearestNodeFromDOMNode(parentTable);
if (table instanceof CustomTableNode) { if (table instanceof TableNode) {
const originalWidth = $getTableColumnWidth(_this.editor, table, cellIndex); const originalWidth = $getTableColumnWidth(_this.editor, table, cellIndex);
const newWidth = Math.max(originalWidth + change, 10); const newWidth = Math.max(originalWidth + change, 10);
$setTableColumnWidth(table, cellIndex, newWidth); $setTableColumnWidth(table, cellIndex, newWidth);

View File

@ -1,12 +1,12 @@
import {$getNodeByKey, LexicalEditor} from "lexical"; import {$getNodeByKey, LexicalEditor} from "lexical";
import {NodeKey} from "lexical/LexicalNode"; import {NodeKey} from "lexical/LexicalNode";
import { import {
$isTableNode,
applyTableHandlers, applyTableHandlers,
HTMLTableElementWithWithTableSelectionState, HTMLTableElementWithWithTableSelectionState,
TableNode, TableNode,
TableObserver TableObserver
} from "@lexical/table"; } from "@lexical/table";
import {$isCustomTableNode, CustomTableNode} from "../../../nodes/custom-table";
// File adapted from logic in: // File adapted from logic in:
// https://github.com/facebook/lexical/blob/f373759a7849f473d34960a6bf4e34b2a011e762/packages/lexical-react/src/LexicalTablePlugin.ts#L49 // https://github.com/facebook/lexical/blob/f373759a7849f473d34960a6bf4e34b2a011e762/packages/lexical-react/src/LexicalTablePlugin.ts#L49
@ -25,12 +25,12 @@ class TableSelectionHandler {
} }
protected init() { protected init() {
this.unregisterMutationListener = this.editor.registerMutationListener(CustomTableNode, (mutations) => { this.unregisterMutationListener = this.editor.registerMutationListener(TableNode, (mutations) => {
for (const [nodeKey, mutation] of mutations) { for (const [nodeKey, mutation] of mutations) {
if (mutation === 'created') { if (mutation === 'created') {
this.editor.getEditorState().read(() => { this.editor.getEditorState().read(() => {
const tableNode = $getNodeByKey<CustomTableNode>(nodeKey); const tableNode = $getNodeByKey<TableNode>(nodeKey);
if ($isCustomTableNode(tableNode)) { if ($isTableNode(tableNode)) {
this.initializeTableNode(tableNode); this.initializeTableNode(tableNode);
} }
}); });

View File

@ -1,5 +1,5 @@
import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical"; import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical";
import {$isCustomListItemNode} from "../../../nodes/custom-list-item"; import {$isListItemNode} from "@lexical/list";
class TaskListHandler { class TaskListHandler {
protected editorContainer: HTMLElement; protected editorContainer: HTMLElement;
@ -38,7 +38,7 @@ class TaskListHandler {
this.editor.update(() => { this.editor.update(() => {
const node = $getNearestNodeFromDOMNode(listItem); const node = $getNearestNodeFromDOMNode(listItem);
if ($isCustomListItemNode(node)) { if ($isListItemNode(node)) {
node.setChecked(!node.getChecked()); node.setChecked(!node.getChecked());
} }
}); });

View File

@ -1,7 +1,7 @@
import {EditorFormModal, EditorFormModalDefinition} from "./modals"; import {EditorFormModal, EditorFormModalDefinition} from "./modals";
import {EditorContainerUiElement, EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core"; import {EditorContainerUiElement, EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core";
import {EditorDecorator, EditorDecoratorAdapter} from "./decorator"; import {EditorDecorator, EditorDecoratorAdapter} from "./decorator";
import {$getSelection, BaseSelection, COMMAND_PRIORITY_LOW, LexicalEditor, SELECTION_CHANGE_COMMAND} from "lexical"; import {BaseSelection, LexicalEditor} from "lexical";
import {DecoratorListener} from "lexical/LexicalEditor"; import {DecoratorListener} from "lexical/LexicalEditor";
import type {NodeKey} from "lexical/LexicalNode"; import type {NodeKey} from "lexical/LexicalNode";
import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars"; import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars";

View File

@ -1,8 +1,8 @@
import {$getSelection, $insertNodes, LexicalEditor, LexicalNode} from "lexical"; import {$insertNodes, LexicalEditor, LexicalNode} from "lexical";
import {HttpError} from "../../services/http"; import {HttpError} from "../../services/http";
import {EditorUiContext} from "../ui/framework/core"; import {EditorUiContext} from "../ui/framework/core";
import * as DrawIO from "../../services/drawio"; import * as DrawIO from "../../services/drawio";
import {$createDiagramNode, DiagramNode} from "../nodes/diagram"; import {$createDiagramNode, DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
import {ImageManager} from "../../components"; import {ImageManager} from "../../components";
import {EditorImageData} from "./images"; import {EditorImageData} from "./images";
import {$getNodeFromSelection, getLastSelection} from "./selection"; import {$getNodeFromSelection, getLastSelection} from "./selection";

View File

@ -1,5 +1,12 @@
import {$isQuoteNode, HeadingNode, HeadingTagType} from "@lexical/rich-text"; import {
import {$createTextNode, $getSelection, $insertNodes, LexicalEditor, LexicalNode} from "lexical"; $createParagraphNode,
$createTextNode,
$getSelection,
$insertNodes,
$isParagraphNode,
LexicalEditor,
LexicalNode
} from "lexical";
import { import {
$getBlockElementNodesInSelection, $getBlockElementNodesInSelection,
$getNodeFromSelection, $getNodeFromSelection,
@ -7,37 +14,35 @@ import {
$toggleSelectionBlockNodeType, $toggleSelectionBlockNodeType,
getLastSelection getLastSelection
} from "./selection"; } from "./selection";
import {$createCustomHeadingNode, $isCustomHeadingNode} from "../nodes/custom-heading"; import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "@lexical/rich-text/LexicalCodeBlockNode";
import {$createCustomParagraphNode, $isCustomParagraphNode} from "../nodes/custom-paragraph"; import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "@lexical/rich-text/LexicalCalloutNode";
import {$createCustomQuoteNode} from "../nodes/custom-quote"; import {$isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list";
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../nodes/code-block";
import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "../nodes/callout";
import {insertList, ListNode, ListType, removeList} from "@lexical/list";
import {$isCustomListNode} from "../nodes/custom-list";
import {$createLinkNode, $isLinkNode} from "@lexical/link"; import {$createLinkNode, $isLinkNode} from "@lexical/link";
import {$createHeadingNode, $isHeadingNode, HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode";
import {$createQuoteNode, $isQuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
const $isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => { const $isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
return $isCustomHeadingNode(node) && (node as HeadingNode).getTag() === tag; return $isHeadingNode(node) && node.getTag() === tag;
}; };
export function toggleSelectionAsHeading(editor: LexicalEditor, tag: HeadingTagType) { export function toggleSelectionAsHeading(editor: LexicalEditor, tag: HeadingTagType) {
editor.update(() => { editor.update(() => {
$toggleSelectionBlockNodeType( $toggleSelectionBlockNodeType(
(node) => $isHeaderNodeOfTag(node, tag), (node) => $isHeaderNodeOfTag(node, tag),
() => $createCustomHeadingNode(tag), () => $createHeadingNode(tag),
) )
}); });
} }
export function toggleSelectionAsParagraph(editor: LexicalEditor) { export function toggleSelectionAsParagraph(editor: LexicalEditor) {
editor.update(() => { editor.update(() => {
$toggleSelectionBlockNodeType($isCustomParagraphNode, $createCustomParagraphNode); $toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode);
}); });
} }
export function toggleSelectionAsBlockquote(editor: LexicalEditor) { export function toggleSelectionAsBlockquote(editor: LexicalEditor) {
editor.update(() => { editor.update(() => {
$toggleSelectionBlockNodeType($isQuoteNode, $createCustomQuoteNode); $toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
}); });
} }
@ -45,7 +50,7 @@ export function toggleSelectionAsList(editor: LexicalEditor, type: ListType) {
editor.getEditorState().read(() => { editor.getEditorState().read(() => {
const selection = $getSelection(); const selection = $getSelection();
const listSelected = $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => { const listSelected = $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
return $isCustomListNode(node) && (node as ListNode).getListType() === type; return $isListNode(node) && (node as ListNode).getListType() === type;
}); });
if (listSelected) { if (listSelected) {

View File

@ -1,5 +1,5 @@
import {ImageManager} from "../../components"; import {ImageManager} from "../../components";
import {$createImageNode} from "../nodes/image"; import {$createImageNode} from "@lexical/rich-text/LexicalImageNode";
import {$createLinkNode, LinkNode} from "@lexical/link"; import {$createLinkNode, LinkNode} from "@lexical/link";
export type EditorImageData = { export type EditorImageData = {

View File

@ -1,22 +1,21 @@
import {$createCustomListItemNode, $isCustomListItemNode, CustomListItemNode} from "../nodes/custom-list-item";
import {$createCustomListNode, $isCustomListNode} from "../nodes/custom-list";
import {$getSelection, BaseSelection, LexicalEditor} from "lexical"; import {$getSelection, BaseSelection, LexicalEditor} from "lexical";
import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection"; import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection";
import {nodeHasInset} from "./nodes"; import {nodeHasInset} from "./nodes";
import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list";
export function $nestListItem(node: CustomListItemNode): CustomListItemNode { export function $nestListItem(node: ListItemNode): ListItemNode {
const list = node.getParent(); const list = node.getParent();
if (!$isCustomListNode(list)) { if (!$isListNode(list)) {
return node; return node;
} }
const listItems = list.getChildren() as CustomListItemNode[]; const listItems = list.getChildren() as ListItemNode[];
const nodeIndex = listItems.findIndex((n) => n.getKey() === node.getKey()); const nodeIndex = listItems.findIndex((n) => n.getKey() === node.getKey());
const isFirst = nodeIndex === 0; const isFirst = nodeIndex === 0;
const newListItem = $createCustomListItemNode(); const newListItem = $createListItemNode();
const newList = $createCustomListNode(list.getListType()); const newList = $createListNode(list.getListType());
newList.append(newListItem); newList.append(newListItem);
newListItem.append(...node.getChildren()); newListItem.append(...node.getChildren());
@ -31,11 +30,11 @@ export function $nestListItem(node: CustomListItemNode): CustomListItemNode {
return newListItem; return newListItem;
} }
export function $unnestListItem(node: CustomListItemNode): CustomListItemNode { export function $unnestListItem(node: ListItemNode): ListItemNode {
const list = node.getParent(); const list = node.getParent();
const parentListItem = list?.getParent(); const parentListItem = list?.getParent();
const outerList = parentListItem?.getParent(); const outerList = parentListItem?.getParent();
if (!$isCustomListNode(list) || !$isCustomListNode(outerList) || !$isCustomListItemNode(parentListItem)) { if (!$isListNode(list) || !$isListNode(outerList) || !$isListItemNode(parentListItem)) {
return node; return node;
} }
@ -51,19 +50,19 @@ export function $unnestListItem(node: CustomListItemNode): CustomListItemNode {
return node; return node;
} }
function getListItemsForSelection(selection: BaseSelection|null): (CustomListItemNode|null)[] { function getListItemsForSelection(selection: BaseSelection|null): (ListItemNode|null)[] {
const nodes = selection?.getNodes() || []; const nodes = selection?.getNodes() || [];
const listItemNodes = []; const listItemNodes = [];
outer: for (const node of nodes) { outer: for (const node of nodes) {
if ($isCustomListItemNode(node)) { if ($isListItemNode(node)) {
listItemNodes.push(node); listItemNodes.push(node);
continue; continue;
} }
const parents = node.getParents(); const parents = node.getParents();
for (const parent of parents) { for (const parent of parents) {
if ($isCustomListItemNode(parent)) { if ($isListItemNode(parent)) {
listItemNodes.push(parent); listItemNodes.push(parent);
continue outer; continue outer;
} }
@ -75,8 +74,8 @@ function getListItemsForSelection(selection: BaseSelection|null): (CustomListIte
return listItemNodes; return listItemNodes;
} }
function $reduceDedupeListItems(listItems: (CustomListItemNode|null)[]): CustomListItemNode[] { function $reduceDedupeListItems(listItems: (ListItemNode|null)[]): ListItemNode[] {
const listItemMap: Record<string, CustomListItemNode> = {}; const listItemMap: Record<string, ListItemNode> = {};
for (const item of listItems) { for (const item of listItems) {
if (item === null) { if (item === null) {

View File

@ -1,4 +1,5 @@
import { import {
$createParagraphNode,
$getRoot, $getRoot,
$isDecoratorNode, $isDecoratorNode,
$isElementNode, $isRootNode, $isElementNode, $isRootNode,
@ -8,16 +9,15 @@ import {
LexicalNode LexicalNode
} from "lexical"; } from "lexical";
import {LexicalNodeMatcher} from "../nodes"; import {LexicalNodeMatcher} from "../nodes";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
import {$generateNodesFromDOM} from "@lexical/html"; import {$generateNodesFromDOM} from "@lexical/html";
import {htmlToDom} from "./dom"; import {htmlToDom} from "./dom";
import {NodeHasAlignment, NodeHasInset} from "../nodes/_common"; import {NodeHasAlignment, NodeHasInset} from "lexical/nodes/common";
import {$findMatchingParent} from "@lexical/utils"; import {$findMatchingParent} from "@lexical/utils";
function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] { function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] {
return nodes.map(node => { return nodes.map(node => {
if ($isTextNode(node)) { if ($isTextNode(node)) {
const paragraph = $createCustomParagraphNode(); const paragraph = $createParagraphNode();
paragraph.append(node); paragraph.append(node);
return paragraph; return paragraph;
} }

View File

@ -7,18 +7,16 @@ import {
$isTextNode, $isTextNode,
$setSelection, $setSelection,
BaseSelection, DecoratorNode, BaseSelection, DecoratorNode,
ElementFormatType,
ElementNode, LexicalEditor, ElementNode, LexicalEditor,
LexicalNode, LexicalNode,
TextFormatType, TextNode TextFormatType, TextNode
} from "lexical"; } from "lexical";
import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexical/utils"; import {$getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes"; import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes";
import {$setBlocksType} from "@lexical/selection"; import {$setBlocksType} from "@lexical/selection";
import {$getNearestNodeBlockParent, $getParentOfType, nodeHasAlignment} from "./nodes"; import {$getNearestNodeBlockParent, $getParentOfType, nodeHasAlignment} from "./nodes";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph"; import {CommonBlockAlignment} from "lexical/nodes/common";
import {CommonBlockAlignment} from "../nodes/_common";
const lastSelectionByEditor = new WeakMap<LexicalEditor, BaseSelection|null>; const lastSelectionByEditor = new WeakMap<LexicalEditor, BaseSelection|null>;
@ -71,7 +69,7 @@ export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creat
const selection = $getSelection(); const selection = $getSelection();
const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null; const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
if (selection && matcher(blockElement)) { if (selection && matcher(blockElement)) {
$setBlocksType(selection, $createCustomParagraphNode); $setBlocksType(selection, $createParagraphNode);
} else { } else {
$setBlocksType(selection, creator); $setBlocksType(selection, creator);
} }

View File

@ -1,24 +1,28 @@
import {NodeClipboard} from "./node-clipboard"; import {NodeClipboard} from "./node-clipboard";
import {CustomTableRowNode} from "../nodes/custom-table-row";
import {$getTableCellsFromSelection, $getTableFromSelection, $getTableRowsFromSelection} from "./tables"; import {$getTableCellsFromSelection, $getTableFromSelection, $getTableRowsFromSelection} from "./tables";
import {$getSelection, BaseSelection, LexicalEditor} from "lexical"; import {$getSelection, BaseSelection, LexicalEditor} from "lexical";
import {$createCustomTableCellNode, $isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell";
import {CustomTableNode} from "../nodes/custom-table";
import {TableMap} from "./table-map"; import {TableMap} from "./table-map";
import {$isTableSelection} from "@lexical/table"; import {
$createTableCellNode,
$isTableCellNode,
$isTableSelection,
TableCellNode,
TableNode,
TableRowNode
} from "@lexical/table";
import {$getNodeFromSelection} from "./selection"; import {$getNodeFromSelection} from "./selection";
const rowClipboard: NodeClipboard<CustomTableRowNode> = new NodeClipboard<CustomTableRowNode>(); const rowClipboard: NodeClipboard<TableRowNode> = new NodeClipboard<TableRowNode>();
export function isRowClipboardEmpty(): boolean { export function isRowClipboardEmpty(): boolean {
return rowClipboard.size() === 0; return rowClipboard.size() === 0;
} }
export function validateRowsToCopy(rows: CustomTableRowNode[]): void { export function validateRowsToCopy(rows: TableRowNode[]): void {
let commonRowSize: number|null = null; let commonRowSize: number|null = null;
for (const row of rows) { for (const row of rows) {
const cells = row.getChildren().filter(n => $isCustomTableCellNode(n)); const cells = row.getChildren().filter(n => $isTableCellNode(n));
let rowSize = 0; let rowSize = 0;
for (const cell of cells) { for (const cell of cells) {
rowSize += cell.getColSpan() || 1; rowSize += cell.getColSpan() || 1;
@ -35,10 +39,10 @@ export function validateRowsToCopy(rows: CustomTableRowNode[]): void {
} }
} }
export function validateRowsToPaste(rows: CustomTableRowNode[], targetTable: CustomTableNode): void { export function validateRowsToPaste(rows: TableRowNode[], targetTable: TableNode): void {
const tableColCount = (new TableMap(targetTable)).columnCount; const tableColCount = (new TableMap(targetTable)).columnCount;
for (const row of rows) { for (const row of rows) {
const cells = row.getChildren().filter(n => $isCustomTableCellNode(n)); const cells = row.getChildren().filter(n => $isTableCellNode(n));
let rowSize = 0; let rowSize = 0;
for (const cell of cells) { for (const cell of cells) {
rowSize += cell.getColSpan() || 1; rowSize += cell.getColSpan() || 1;
@ -49,7 +53,7 @@ export function validateRowsToPaste(rows: CustomTableRowNode[], targetTable: Cus
} }
while (rowSize < tableColCount) { while (rowSize < tableColCount) {
row.append($createCustomTableCellNode()); row.append($createTableCellNode());
rowSize++; rowSize++;
} }
} }
@ -98,11 +102,11 @@ export function $pasteClipboardRowsAfter(editor: LexicalEditor): void {
} }
} }
const columnClipboard: NodeClipboard<CustomTableCellNode>[] = []; const columnClipboard: NodeClipboard<TableCellNode>[] = [];
function setColumnClipboard(columns: CustomTableCellNode[][]): void { function setColumnClipboard(columns: TableCellNode[][]): void {
const newClipboards = columns.map(cells => { const newClipboards = columns.map(cells => {
const clipboard = new NodeClipboard<CustomTableCellNode>(); const clipboard = new NodeClipboard<TableCellNode>();
clipboard.set(...cells); clipboard.set(...cells);
return clipboard; return clipboard;
}); });
@ -122,9 +126,9 @@ function $getSelectionColumnRange(selection: BaseSelection|null): TableRange|nul
return {from: shape.fromX, to: shape.toX}; return {from: shape.fromX, to: shape.toX};
} }
const cell = $getNodeFromSelection(selection, $isCustomTableCellNode); const cell = $getNodeFromSelection(selection, $isTableCellNode);
const table = $getTableFromSelection(selection); const table = $getTableFromSelection(selection);
if (!$isCustomTableCellNode(cell) || !table) { if (!$isTableCellNode(cell) || !table) {
return null; return null;
} }
@ -137,7 +141,7 @@ function $getSelectionColumnRange(selection: BaseSelection|null): TableRange|nul
return {from: range.fromX, to: range.toX}; return {from: range.fromX, to: range.toX};
} }
function $getTableColumnCellsFromSelection(range: TableRange, table: CustomTableNode): CustomTableCellNode[][] { function $getTableColumnCellsFromSelection(range: TableRange, table: TableNode): TableCellNode[][] {
const map = new TableMap(table); const map = new TableMap(table);
const columns = []; const columns = [];
for (let x = range.from; x <= range.to; x++) { for (let x = range.from; x <= range.to; x++) {
@ -148,7 +152,7 @@ function $getTableColumnCellsFromSelection(range: TableRange, table: CustomTable
return columns; return columns;
} }
function validateColumnsToCopy(columns: CustomTableCellNode[][]): void { function validateColumnsToCopy(columns: TableCellNode[][]): void {
let commonColSize: number|null = null; let commonColSize: number|null = null;
for (const cells of columns) { for (const cells of columns) {
@ -203,7 +207,7 @@ export function $copySelectedColumnsToClipboard(): void {
setColumnClipboard(columns); setColumnClipboard(columns);
} }
function validateColumnsToPaste(columns: CustomTableCellNode[][], targetTable: CustomTableNode) { function validateColumnsToPaste(columns: TableCellNode[][], targetTable: TableNode) {
const tableRowCount = (new TableMap(targetTable)).rowCount; const tableRowCount = (new TableMap(targetTable)).rowCount;
for (const cells of columns) { for (const cells of columns) {
let colSize = 0; let colSize = 0;
@ -216,7 +220,7 @@ function validateColumnsToPaste(columns: CustomTableCellNode[][], targetTable: C
} }
while (colSize < tableRowCount) { while (colSize < tableRowCount) {
cells.push($createCustomTableCellNode()); cells.push($createTableCellNode());
colSize++; colSize++;
} }
} }

View File

@ -1,6 +1,4 @@
import {CustomTableNode} from "../nodes/custom-table"; import {$isTableCellNode, $isTableRowNode, TableCellNode, TableNode} from "@lexical/table";
import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell";
import {$isTableRowNode} from "@lexical/table";
export type CellRange = { export type CellRange = {
fromX: number; fromX: number;
@ -16,15 +14,15 @@ export class TableMap {
// Represents an array (rows*columns in length) of cell nodes from top-left to // Represents an array (rows*columns in length) of cell nodes from top-left to
// bottom right. Cells may repeat where merged and covering multiple spaces. // bottom right. Cells may repeat where merged and covering multiple spaces.
cells: CustomTableCellNode[] = []; cells: TableCellNode[] = [];
constructor(table: CustomTableNode) { constructor(table: TableNode) {
this.buildCellMap(table); this.buildCellMap(table);
} }
protected buildCellMap(table: CustomTableNode) { protected buildCellMap(table: TableNode) {
const rowsAndCells: CustomTableCellNode[][] = []; const rowsAndCells: TableCellNode[][] = [];
const setCell = (x: number, y: number, cell: CustomTableCellNode) => { const setCell = (x: number, y: number, cell: TableCellNode) => {
if (typeof rowsAndCells[y] === 'undefined') { if (typeof rowsAndCells[y] === 'undefined') {
rowsAndCells[y] = []; rowsAndCells[y] = [];
} }
@ -36,7 +34,7 @@ export class TableMap {
const rowNodes = table.getChildren().filter(r => $isTableRowNode(r)); const rowNodes = table.getChildren().filter(r => $isTableRowNode(r));
for (let rowIndex = 0; rowIndex < rowNodes.length; rowIndex++) { for (let rowIndex = 0; rowIndex < rowNodes.length; rowIndex++) {
const rowNode = rowNodes[rowIndex]; const rowNode = rowNodes[rowIndex];
const cellNodes = rowNode.getChildren().filter(c => $isCustomTableCellNode(c)); const cellNodes = rowNode.getChildren().filter(c => $isTableCellNode(c));
let targetColIndex: number = 0; let targetColIndex: number = 0;
for (let cellIndex = 0; cellIndex < cellNodes.length; cellIndex++) { for (let cellIndex = 0; cellIndex < cellNodes.length; cellIndex++) {
const cellNode = cellNodes[cellIndex]; const cellNode = cellNodes[cellIndex];
@ -60,7 +58,7 @@ export class TableMap {
this.columnCount = Math.max(...rowsAndCells.map(r => r.length)); this.columnCount = Math.max(...rowsAndCells.map(r => r.length));
const cells = []; const cells = [];
let lastCell: CustomTableCellNode = rowsAndCells[0][0]; let lastCell: TableCellNode = rowsAndCells[0][0];
for (let y = 0; y < this.rowCount; y++) { for (let y = 0; y < this.rowCount; y++) {
for (let x = 0; x < this.columnCount; x++) { for (let x = 0; x < this.columnCount; x++) {
if (!rowsAndCells[y] || !rowsAndCells[y][x]) { if (!rowsAndCells[y] || !rowsAndCells[y][x]) {
@ -75,7 +73,7 @@ export class TableMap {
this.cells = cells; this.cells = cells;
} }
public getCellAtPosition(x: number, y: number): CustomTableCellNode { public getCellAtPosition(x: number, y: number): TableCellNode {
const position = (y * this.columnCount) + x; const position = (y * this.columnCount) + x;
if (position >= this.cells.length) { if (position >= this.cells.length) {
throw new Error(`TableMap Error: Attempted to get cell ${position+1} of ${this.cells.length}`); throw new Error(`TableMap Error: Attempted to get cell ${position+1} of ${this.cells.length}`);
@ -84,13 +82,13 @@ export class TableMap {
return this.cells[position]; return this.cells[position];
} }
public getCellsInRange(range: CellRange): CustomTableCellNode[] { public getCellsInRange(range: CellRange): TableCellNode[] {
const minX = Math.max(Math.min(range.fromX, range.toX), 0); const minX = Math.max(Math.min(range.fromX, range.toX), 0);
const maxX = Math.min(Math.max(range.fromX, range.toX), this.columnCount - 1); const maxX = Math.min(Math.max(range.fromX, range.toX), this.columnCount - 1);
const minY = Math.max(Math.min(range.fromY, range.toY), 0); const minY = Math.max(Math.min(range.fromY, range.toY), 0);
const maxY = Math.min(Math.max(range.fromY, range.toY), this.rowCount - 1); const maxY = Math.min(Math.max(range.fromY, range.toY), this.rowCount - 1);
const cells = new Set<CustomTableCellNode>(); const cells = new Set<TableCellNode>();
for (let y = minY; y <= maxY; y++) { for (let y = minY; y <= maxY; y++) {
for (let x = minX; x <= maxX; x++) { for (let x = minX; x <= maxX; x++) {
@ -101,7 +99,7 @@ export class TableMap {
return [...cells.values()]; return [...cells.values()];
} }
public getCellsInColumn(columnIndex: number): CustomTableCellNode[] { public getCellsInColumn(columnIndex: number): TableCellNode[] {
return this.getCellsInRange({ return this.getCellsInRange({
fromX: columnIndex, fromX: columnIndex,
toX: columnIndex, toX: columnIndex,
@ -110,7 +108,7 @@ export class TableMap {
}); });
} }
public getRangeForCell(cell: CustomTableCellNode): CellRange|null { public getRangeForCell(cell: TableCellNode): CellRange|null {
let range: CellRange|null = null; let range: CellRange|null = null;
const cellKey = cell.getKey(); const cellKey = cell.getKey();

View File

@ -1,15 +1,19 @@
import {BaseSelection, LexicalEditor} from "lexical"; import {BaseSelection, LexicalEditor} from "lexical";
import {$isTableRowNode, $isTableSelection, TableRowNode, TableSelection, TableSelectionShape} from "@lexical/table"; import {
import {$isCustomTableNode, CustomTableNode} from "../nodes/custom-table"; $isTableCellNode,
import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell"; $isTableNode,
$isTableRowNode,
$isTableSelection, TableCellNode, TableNode,
TableRowNode,
TableSelection,
} from "@lexical/table";
import {$getParentOfType} from "./nodes"; import {$getParentOfType} from "./nodes";
import {$getNodeFromSelection} from "./selection"; import {$getNodeFromSelection} from "./selection";
import {formatSizeValue} from "./dom"; import {formatSizeValue} from "./dom";
import {TableMap} from "./table-map"; import {TableMap} from "./table-map";
import {$isCustomTableRowNode, CustomTableRowNode} from "../nodes/custom-table-row";
function $getTableFromCell(cell: CustomTableCellNode): CustomTableNode|null { function $getTableFromCell(cell: TableCellNode): TableNode|null {
return $getParentOfType(cell, $isCustomTableNode) as CustomTableNode|null; return $getParentOfType(cell, $isTableNode) as TableNode|null;
} }
export function getTableColumnWidths(table: HTMLTableElement): string[] { export function getTableColumnWidths(table: HTMLTableElement): string[] {
@ -55,7 +59,7 @@ function extractWidthFromElement(element: HTMLElement): string {
return width || ''; return width || '';
} }
export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number, width: number|string): void { export function $setTableColumnWidth(node: TableNode, columnIndex: number, width: number|string): void {
const rows = node.getChildren() as TableRowNode[]; const rows = node.getChildren() as TableRowNode[];
let maxCols = 0; let maxCols = 0;
for (const row of rows) { for (const row of rows) {
@ -78,7 +82,7 @@ export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number,
node.setColWidths(colWidths); node.setColWidths(colWidths);
} }
export function $getTableColumnWidth(editor: LexicalEditor, node: CustomTableNode, columnIndex: number): number { export function $getTableColumnWidth(editor: LexicalEditor, node: TableNode, columnIndex: number): number {
const colWidths = node.getColWidths(); const colWidths = node.getColWidths();
if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) { if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) {
return Number(colWidths[columnIndex].replace('px', '')); return Number(colWidths[columnIndex].replace('px', ''));
@ -97,14 +101,14 @@ export function $getTableColumnWidth(editor: LexicalEditor, node: CustomTableNod
return 0; return 0;
} }
function $getCellColumnIndex(node: CustomTableCellNode): number { function $getCellColumnIndex(node: TableCellNode): number {
const row = node.getParent(); const row = node.getParent();
if (!$isTableRowNode(row)) { if (!$isTableRowNode(row)) {
return -1; return -1;
} }
let index = 0; let index = 0;
const cells = row.getChildren<CustomTableCellNode>(); const cells = row.getChildren<TableCellNode>();
for (const cell of cells) { for (const cell of cells) {
let colSpan = cell.getColSpan() || 1; let colSpan = cell.getColSpan() || 1;
index += colSpan; index += colSpan;
@ -116,7 +120,7 @@ function $getCellColumnIndex(node: CustomTableCellNode): number {
return index - 1; return index - 1;
} }
export function $setTableCellColumnWidth(cell: CustomTableCellNode, width: string): void { export function $setTableCellColumnWidth(cell: TableCellNode, width: string): void {
const table = $getTableFromCell(cell) const table = $getTableFromCell(cell)
const index = $getCellColumnIndex(cell); const index = $getCellColumnIndex(cell);
@ -125,7 +129,7 @@ export function $setTableCellColumnWidth(cell: CustomTableCellNode, width: strin
} }
} }
export function $getTableCellColumnWidth(editor: LexicalEditor, cell: CustomTableCellNode): string { export function $getTableCellColumnWidth(editor: LexicalEditor, cell: TableCellNode): string {
const table = $getTableFromCell(cell) const table = $getTableFromCell(cell)
const index = $getCellColumnIndex(cell); const index = $getCellColumnIndex(cell);
if (!table) { if (!table) {
@ -136,13 +140,13 @@ export function $getTableCellColumnWidth(editor: LexicalEditor, cell: CustomTabl
return (widths.length > index) ? widths[index] : ''; return (widths.length > index) ? widths[index] : '';
} }
export function $getTableCellsFromSelection(selection: BaseSelection|null): CustomTableCellNode[] { export function $getTableCellsFromSelection(selection: BaseSelection|null): TableCellNode[] {
if ($isTableSelection(selection)) { if ($isTableSelection(selection)) {
const nodes = selection.getNodes(); const nodes = selection.getNodes();
return nodes.filter(n => $isCustomTableCellNode(n)); return nodes.filter(n => $isTableCellNode(n));
} }
const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as CustomTableCellNode; const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode;
return cell ? [cell] : []; return cell ? [cell] : [];
} }
@ -193,12 +197,12 @@ export function $mergeTableCellsInSelection(selection: TableSelection): void {
firstCell.setRowSpan(newHeight); firstCell.setRowSpan(newHeight);
} }
export function $getTableRowsFromSelection(selection: BaseSelection|null): CustomTableRowNode[] { export function $getTableRowsFromSelection(selection: BaseSelection|null): TableRowNode[] {
const cells = $getTableCellsFromSelection(selection); const cells = $getTableCellsFromSelection(selection);
const rowsByKey: Record<string, CustomTableRowNode> = {}; const rowsByKey: Record<string, TableRowNode> = {};
for (const cell of cells) { for (const cell of cells) {
const row = cell.getParent(); const row = cell.getParent();
if ($isCustomTableRowNode(row)) { if ($isTableRowNode(row)) {
rowsByKey[row.getKey()] = row; rowsByKey[row.getKey()] = row;
} }
} }
@ -206,28 +210,28 @@ export function $getTableRowsFromSelection(selection: BaseSelection|null): Custo
return Object.values(rowsByKey); return Object.values(rowsByKey);
} }
export function $getTableFromSelection(selection: BaseSelection|null): CustomTableNode|null { export function $getTableFromSelection(selection: BaseSelection|null): TableNode|null {
const cells = $getTableCellsFromSelection(selection); const cells = $getTableCellsFromSelection(selection);
if (cells.length === 0) { if (cells.length === 0) {
return null; return null;
} }
const table = $getParentOfType(cells[0], $isCustomTableNode); const table = $getParentOfType(cells[0], $isTableNode);
if ($isCustomTableNode(table)) { if ($isTableNode(table)) {
return table; return table;
} }
return null; return null;
} }
export function $clearTableSizes(table: CustomTableNode): void { export function $clearTableSizes(table: TableNode): void {
table.setColWidths([]); table.setColWidths([]);
// TODO - Extra form things once table properties and extra things // TODO - Extra form things once table properties and extra things
// are supported // are supported
for (const row of table.getChildren()) { for (const row of table.getChildren()) {
if (!$isCustomTableRowNode(row)) { if (!$isTableRowNode(row)) {
continue; continue;
} }
@ -236,7 +240,7 @@ export function $clearTableSizes(table: CustomTableNode): void {
rowStyles.delete('width'); rowStyles.delete('width');
row.setStyles(rowStyles); row.setStyles(rowStyles);
const cells = row.getChildren().filter(c => $isCustomTableCellNode(c)); const cells = row.getChildren().filter(c => $isTableCellNode(c));
for (const cell of cells) { for (const cell of cells) {
const cellStyles = cell.getStyles(); const cellStyles = cell.getStyles();
cellStyles.delete('height'); cellStyles.delete('height');
@ -247,23 +251,21 @@ export function $clearTableSizes(table: CustomTableNode): void {
} }
} }
export function $clearTableFormatting(table: CustomTableNode): void { export function $clearTableFormatting(table: TableNode): void {
table.setColWidths([]); table.setColWidths([]);
table.setStyles(new Map); table.setStyles(new Map);
for (const row of table.getChildren()) { for (const row of table.getChildren()) {
if (!$isCustomTableRowNode(row)) { if (!$isTableRowNode(row)) {
continue; continue;
} }
row.setStyles(new Map); row.setStyles(new Map);
row.setFormat('');
const cells = row.getChildren().filter(c => $isCustomTableCellNode(c)); const cells = row.getChildren().filter(c => $isTableCellNode(c));
for (const cell of cells) { for (const cell of cells) {
cell.setStyles(new Map); cell.setStyles(new Map);
cell.clearWidth(); cell.clearWidth();
cell.setFormat('');
} }
} }
} }
@ -272,14 +274,14 @@ export function $clearTableFormatting(table: CustomTableNode): void {
* Perform the given callback for each cell in the given table. * Perform the given callback for each cell in the given table.
* Returning false from the callback stops the function early. * Returning false from the callback stops the function early.
*/ */
export function $forEachTableCell(table: CustomTableNode, callback: (c: CustomTableCellNode) => void|false): void { export function $forEachTableCell(table: TableNode, callback: (c: TableCellNode) => void|false): void {
outer: for (const row of table.getChildren()) { outer: for (const row of table.getChildren()) {
if (!$isCustomTableRowNode(row)) { if (!$isTableRowNode(row)) {
continue; continue;
} }
const cells = row.getChildren(); const cells = row.getChildren();
for (const cell of cells) { for (const cell of cells) {
if (!$isCustomTableCellNode(cell)) { if (!$isTableCellNode(cell)) {
return; return;
} }
const result = callback(cell); const result = callback(cell);
@ -290,10 +292,10 @@ export function $forEachTableCell(table: CustomTableNode, callback: (c: CustomTa
} }
} }
export function $getCellPaddingForTable(table: CustomTableNode): string { export function $getCellPaddingForTable(table: TableNode): string {
let padding: string|null = null; let padding: string|null = null;
$forEachTableCell(table, (cell: CustomTableCellNode) => { $forEachTableCell(table, (cell: TableCellNode) => {
const cellPadding = cell.getStyles().get('padding') || '' const cellPadding = cell.getStyles().get('padding') || ''
if (padding === null) { if (padding === null) {
padding = cellPadding; padding = cellPadding;