0
0
Fork 0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-04-16 17:47:52 +00:00

Merge pull request 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
resources/js/wysiwyg
index.ts
lexical
nodes.ts
nodes
services
todo.md
ui
utils

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 {registerRichText} from '@lexical/rich-text';
import {mergeRegister} from '@lexical/utils';

View file

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

View file

@ -16,7 +16,6 @@ import {
$getSelection,
$isDecoratorNode,
$isElementNode,
$isRangeSelection,
$isTextNode,
$setSelection,
} from '.';
@ -96,15 +95,6 @@ function shouldUpdateTextNodeFromMutation(
targetDOM: Node,
targetNode: TextNode,
): 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();
}

View file

@ -17,7 +17,6 @@ import type {NodeKey, NodeMap} from './LexicalNode';
import type {ElementNode} from './nodes/LexicalElementNode';
import invariant from 'lexical/shared/invariant';
import normalizeClassNames from 'lexical/shared/normalizeClassNames';
import {
$isDecoratorNode,
@ -30,12 +29,12 @@ import {
import {
DOUBLE_LINE_BREAK,
FULL_RECONCILE,
IS_ALIGN_CENTER,
IS_ALIGN_END,
IS_ALIGN_JUSTIFY,
IS_ALIGN_LEFT,
IS_ALIGN_RIGHT,
IS_ALIGN_START,
} from './LexicalConstants';
import {EditorState} from './LexicalEditorState';
import {
@ -117,51 +116,6 @@ function setTextAlign(domStyle: CSSStyleDeclaration, value: string): void {
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(
key: NodeKey,
parentDOM: null | HTMLElement,
@ -185,22 +139,14 @@ function $createNode(
}
if ($isElementNode(node)) {
const indent = node.__indent;
const childrenSize = node.__size;
if (indent !== 0) {
setElementIndent(dom, indent);
}
if (childrenSize !== 0) {
const endIndex = childrenSize - 1;
const children = createChildrenArray(node, activeNextNodeMap);
$createChildren(children, node, 0, endIndex, dom, null);
}
const format = node.__format;
if (format !== 0) {
setElementFormat(dom, format);
}
if (!node.isInline()) {
reconcileElementTerminatingLineBreak(null, node, dom);
}
@ -349,10 +295,8 @@ function reconcileParagraphFormat(element: ElementNode): void {
if (
$isParagraphNode(element) &&
subTreeTextFormat != null &&
subTreeTextFormat !== element.__textFormat &&
!activeEditorStateReadOnly
) {
element.setTextFormat(subTreeTextFormat);
element.setTextStyle(subTreeTextStyle);
}
}
@ -563,17 +507,6 @@ function $reconcileNode(
if ($isElementNode(prevNode) && $isElementNode(nextNode)) {
// 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) {
$reconcileChildrenWithDirection(prevNode, nextNode, dom);
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 {
$createTestDecoratorNode,
$createTestElementNode,
$createTestInlineElementNode,
createTestEditor,
@ -975,7 +974,7 @@ describe('LexicalEditor tests', () => {
editable ? 'editable' : 'non-editable'
})`, async () => {
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();
const contentEditable = editor.getRootElement();
editor.setEditable(editable);
@ -1048,8 +1047,6 @@ describe('LexicalEditor tests', () => {
__cachedText: null,
__dir: null,
__first: paragraphKey,
__format: 0,
__indent: 0,
__key: 'root',
__last: paragraphKey,
__next: null,
@ -1060,10 +1057,11 @@ describe('LexicalEditor tests', () => {
__type: 'root',
});
expect(parsedParagraph).toEqual({
"__alignment": "",
__dir: null,
__first: textKey,
__format: 0,
__indent: 0,
__id: '',
__inset: 0,
__key: paragraphKey,
__last: textKey,
__next: null,
@ -1071,7 +1069,6 @@ describe('LexicalEditor tests', () => {
__prev: null,
__size: 1,
__style: '',
__textFormat: 0,
__textStyle: '',
__type: 'paragraph',
});
@ -1130,8 +1127,6 @@ describe('LexicalEditor tests', () => {
__cachedText: null,
__dir: null,
__first: paragraphKey,
__format: 0,
__indent: 0,
__key: 'root',
__last: paragraphKey,
__next: null,
@ -1142,10 +1137,11 @@ describe('LexicalEditor tests', () => {
__type: 'root',
});
expect(parsedParagraph).toEqual({
"__alignment": "",
__dir: null,
__first: textKey,
__format: 0,
__indent: 0,
__id: '',
__inset: 0,
__key: paragraphKey,
__last: textKey,
__next: null,
@ -1153,7 +1149,6 @@ describe('LexicalEditor tests', () => {
__prev: null,
__size: 1,
__style: '',
__textFormat: 0,
__textStyle: '',
__type: 'paragraph',
});

View file

@ -54,8 +54,6 @@ describe('LexicalEditorState tests', () => {
__cachedText: 'foo',
__dir: null,
__first: '1',
__format: 0,
__indent: 0,
__key: 'root',
__last: '1',
__next: null,
@ -66,10 +64,11 @@ describe('LexicalEditorState tests', () => {
__type: 'root',
});
expect(paragraph).toEqual({
"__alignment": "",
__dir: null,
__first: '2',
__format: 0,
__indent: 0,
__id: '',
__inset: 0,
__key: '1',
__last: '2',
__next: null,
@ -77,7 +76,6 @@ describe('LexicalEditorState tests', () => {
__prev: null,
__size: 1,
__style: '',
__textFormat: 0,
__textStyle: '',
__type: 'paragraph',
});
@ -113,7 +111,7 @@ describe('LexicalEditorState tests', () => {
});
expect(JSON.stringify(editor.getEditorState().toJSON())).toEqual(
`{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Hello world","type":"text","version":1}],"direction":null,"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: '',
__dir: null,
__first: null,
__format: 0,
__indent: 0,
__key: 'root',
__last: 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 {ListItemNode, ListNode} from '@lexical/list';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
import {
@ -36,6 +35,8 @@ import {
LexicalNodeReplacement,
} from '../../LexicalEditor';
import {resetRandomKey} from '../../LexicalUtils';
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
type TestEnv = {
@ -129,8 +130,6 @@ export class TestElementNode extends ElementNode {
serializedNode: SerializedTestElementNode,
): TestInlineElementNode {
const node = $createTestInlineElementNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);
return node;
}
@ -195,8 +194,6 @@ export class TestInlineElementNode extends ElementNode {
serializedNode: SerializedTestInlineElementNode,
): TestInlineElementNode {
const node = $createTestInlineElementNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);
return node;
}
@ -241,8 +238,6 @@ export class TestShadowRootNode extends ElementNode {
serializedNode: SerializedTestShadowRootNode,
): TestShadowRootNode {
const node = $createTestShadowRootNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);
return node;
}
@ -322,8 +317,6 @@ export class TestExcludeFromCopyElementNode extends ElementNode {
serializedNode: SerializedTestExcludeFromCopyElementNode,
): TestExcludeFromCopyElementNode {
const node = $createTestExcludeFromCopyElementNode();
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);
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 {
DOUBLE_LINE_BREAK,
ELEMENT_FORMAT_TO_TYPE,
ELEMENT_TYPE_TO_FORMAT,
} from '../LexicalConstants';
import {LexicalNode} from '../LexicalNode';
import {
@ -42,8 +42,6 @@ export type SerializedElementNode<
{
children: Array<T>;
direction: 'ltr' | 'rtl' | null;
format: ElementFormatType;
indent: number;
},
SerializedLexicalNode
>;
@ -74,12 +72,8 @@ export class ElementNode extends LexicalNode {
/** @internal */
__size: number;
/** @internal */
__format: number;
/** @internal */
__style: string;
/** @internal */
__indent: number;
/** @internal */
__dir: 'ltr' | 'rtl' | null;
constructor(key?: NodeKey) {
@ -87,9 +81,7 @@ export class ElementNode extends LexicalNode {
this.__first = null;
this.__last = null;
this.__size = 0;
this.__format = 0;
this.__style = '';
this.__indent = 0;
this.__dir = null;
}
@ -98,28 +90,14 @@ export class ElementNode extends LexicalNode {
this.__first = prevNode.__first;
this.__last = prevNode.__last;
this.__size = prevNode.__size;
this.__indent = prevNode.__indent;
this.__format = prevNode.__format;
this.__style = prevNode.__style;
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 {
const self = this.getLatest();
return self.__style;
}
getIndent(): number {
const self = this.getLatest();
return self.__indent;
}
getChildren<T extends LexicalNode>(): Array<T> {
const children: Array<T> = [];
let child: T | null = this.getFirstChild();
@ -301,13 +279,6 @@ export class ElementNode extends LexicalNode {
const self = this.getLatest();
return self.__dir;
}
hasFormat(type: ElementFormatType): boolean {
if (type !== '') {
const formatFlag = ELEMENT_TYPE_TO_FORMAT[type];
return (this.getFormat() & formatFlag) !== 0;
}
return false;
}
// Mutators
@ -378,21 +349,11 @@ export class ElementNode extends LexicalNode {
self.__dir = direction;
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 {
const self = this.getWritable();
self.__style = style || '';
return this;
}
setIndent(indentLevel: number): this {
const self = this.getWritable();
self.__indent = indentLevel;
return this;
}
splice(
start: number,
deleteCount: number,
@ -528,8 +489,6 @@ export class ElementNode extends LexicalNode {
return {
children: [],
direction: this.getDirection(),
format: this.getFormatType(),
indent: this.getIndent(),
type: 'element',
version: 1,
};

View file

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

View file

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

View file

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

View file

@ -48,11 +48,11 @@ describe('LexicalParagraphNode tests', () => {
// logic is in place in the corresponding importJSON method
// to accomodate these changes.
expect(node.exportJSON()).toStrictEqual({
alignment: '',
children: [],
direction: null,
format: '',
indent: 0,
textFormat: 0,
id: '',
inset: 0,
textStyle: '',
type: 'paragraph',
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 () => {
const {editor} = testEnv;

View file

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

View file

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

View file

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

View file

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

View file

@ -206,7 +206,7 @@ describe('LexicalHeadlessEditor', () => {
cleanup();
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 {LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {
$createParagraphNode,
$createRangeSelection,
$createTextNode,
$getRoot,
} from 'lexical';
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
describe('HTML', () => {
type Input = Array<{
@ -175,7 +176,7 @@ describe('HTML', () => {
});
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(
'<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++) {
const node = nodes[i];
if ($isBlockElementNode(node)) {
if (textAlign && !node.getFormat()) {
node.setFormat(textAlign);
}
out.push(node);
} else {
continuousInlines.push(node);
@ -338,7 +335,6 @@ function wrapContinuousInlines(
(i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1]))
) {
const wrapper = createWrapperFn();
wrapper.setFormat(textAlign);
wrapper.append(...continuousInlines);
out.push(wrapper);
continuousInlines = [];

View file

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

View file

@ -13,7 +13,6 @@ import type {
DOMConversionOutput,
DOMExportOutput,
EditorConfig,
EditorThemeClasses,
LexicalNode,
NodeKey,
ParagraphNode,
@ -22,10 +21,6 @@ import type {
Spread,
} from 'lexical';
import {
addClassNamesToElement,
removeClassNamesFromElement,
} from '@lexical/utils';
import {
$applyNodeReplacement,
$createParagraphNode,
@ -36,11 +31,11 @@ import {
LexicalEditor,
} from 'lexical';
import invariant from 'lexical/shared/invariant';
import normalizeClassNames from 'lexical/shared/normalizeClassNames';
import {$createListNode, $isListNode} from './';
import {$handleIndent, $handleOutdent, mergeLists} from './formatList';
import {mergeLists} from './formatList';
import {isNestedListNode} from './utils';
import {el} from "../../utils/dom";
export type SerializedListItemNode = Spread<
{
@ -74,11 +69,17 @@ export class ListItemNode extends ElementNode {
createDOM(config: EditorConfig): HTMLElement {
const element = document.createElement('li');
const parent = this.getParent();
if ($isListNode(parent) && parent.getListType() === 'check') {
updateListItemChecked(element, this, null, parent);
updateListItemChecked(element, this);
}
element.value = this.__value;
$setListItemThemeClassNames(element, config.theme, this);
if ($hasNestedListWithoutLabel(this)) {
element.style.listStyle = 'none';
}
return element;
}
@ -89,11 +90,12 @@ export class ListItemNode extends ElementNode {
): boolean {
const parent = this.getParent();
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
dom.value = this.__value;
$setListItemThemeClassNames(dom, config.theme, this);
return false;
}
@ -126,14 +128,26 @@ export class ListItemNode extends ElementNode {
const node = $createListItemNode();
node.setChecked(serializedNode.checked);
node.setValue(serializedNode.value);
node.setFormat(serializedNode.format);
node.setDirection(serializedNode.direction);
return node;
}
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,
};
@ -172,7 +186,6 @@ export class ListItemNode extends ElementNode {
if ($isListItemNode(replaceWithNode)) {
return super.replace(replaceWithNode);
}
this.setIndent(0);
const list = this.getParentOrThrow();
if (!$isListNode(list)) {
return replaceWithNode;
@ -351,41 +364,6 @@ export class ListItemNode extends ElementNode {
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 */
canInsertAfter(node: LexicalNode): boolean {
return $isListItemNode(node);
@ -428,89 +406,33 @@ export class ListItemNode extends ElementNode {
}
}
function $setListItemThemeClassNames(
dom: HTMLElement,
editorThemeClasses: EditorThemeClasses,
node: ListItemNode,
): void {
const classesToAdd = [];
const classesToRemove = [];
const listTheme = editorThemeClasses.list;
const listItemClassName = listTheme ? listTheme.listitem : undefined;
let nestedListItemClassName;
function $hasNestedListWithoutLabel(node: ListItemNode): boolean {
const children = node.getChildren();
let hasLabel = false;
let hasNestedList = false;
if (listTheme && listTheme.nested) {
nestedListItemClassName = listTheme.nested.listitem;
}
if (listItemClassName !== undefined) {
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,
);
for (const child of children) {
if ($isListNode(child)) {
hasNestedList = true;
} else if (child.getTextContent().trim().length > 0) {
hasLabel = true;
}
}
if (nestedListItemClassName !== undefined) {
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);
}
return hasNestedList && !hasLabel;
}
function updateListItemChecked(
dom: HTMLElement,
listItemNode: ListItemNode,
prevListItemNode: ListItemNode | null,
listNode: ListNode,
): void {
// Only add attributes for leaf list items
if ($isListNode(listItemNode.getFirstChild())) {
dom.removeAttribute('role');
dom.removeAttribute('tabIndex');
dom.removeAttribute('aria-checked');
// 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.setAttribute('role', 'checkbox');
dom.setAttribute('tabIndex', '-1');
if (
!prevListItemNode ||
listItemNode.__checked !== prevListItemNode.__checked
) {
dom.setAttribute(
'aria-checked',
listItemNode.getChecked() ? 'true' : 'false',
);
}
dom.removeAttribute('checked');
}
}

View file

@ -36,9 +36,11 @@ import {
updateChildrenListItemValue,
} from './formatList';
import {$getListDepth, $wrapInListItem} from './utils';
import {extractDirectionFromElement} from "lexical/nodes/common";
export type SerializedListNode = Spread<
{
id: string;
listType: ListType;
start: number;
tag: ListNodeTagType;
@ -58,15 +60,18 @@ export class ListNode extends ElementNode {
__start: number;
/** @internal */
__listType: ListType;
/** @internal */
__id: string = '';
static getType(): string {
return 'list';
}
static clone(node: ListNode): ListNode {
const listType = node.__listType || TAG_TO_LIST_TYPE[node.__tag];
return new ListNode(listType, node.__start, node.__key);
const newNode = new ListNode(node.__listType, node.__start, node.__key);
newNode.__id = node.__id;
newNode.__dir = node.__dir;
return newNode;
}
constructor(listType: ListType, start: number, key?: NodeKey) {
@ -81,6 +86,16 @@ export class ListNode extends ElementNode {
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 {
const writable = this.getWritable();
writable.__listType = type;
@ -108,6 +123,14 @@ export class ListNode extends ElementNode {
dom.__lexicalListType = this.__listType;
$setListThemeClassNames(dom, config.theme, this);
if (this.__id) {
dom.setAttribute('id', this.__id);
}
if (this.__dir) {
dom.setAttribute('dir', this.__dir);
}
return dom;
}
@ -116,7 +139,11 @@ export class ListNode extends ElementNode {
dom: HTMLElement,
config: EditorConfig,
): boolean {
if (prevNode.__tag !== this.__tag) {
if (
prevNode.__tag !== this.__tag
|| prevNode.__dir !== this.__dir
|| prevNode.__id !== this.__id
) {
return true;
}
@ -148,8 +175,7 @@ export class ListNode extends ElementNode {
static importJSON(serializedNode: SerializedListNode): ListNode {
const node = $createListNode(serializedNode.listType, serializedNode.start);
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setId(serializedNode.id);
node.setDirection(serializedNode.direction);
return node;
}
@ -177,6 +203,7 @@ export class ListNode extends ElementNode {
tag: this.getTag(),
type: 'list',
version: 1,
id: this.__id,
};
}
@ -277,28 +304,21 @@ function $setListThemeClassNames(
}
/*
* This function normalizes the children of a ListNode after the conversion from HTML,
* ensuring that they are all ListItemNodes and contain either a single nested ListNode
* or some other inline content.
* 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.
*/
function $normalizeChildren(nodes: Array<LexicalNode>): 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)) {
normalizedListItems.push(node);
const children = node.getChildren();
if (children.length > 1) {
children.forEach((child) => {
if ($isListNode(child)) {
normalizedListItems.push($wrapInListItem(child));
}
});
}
} else {
normalizedListItems.push($wrapInListItem(node));
}
}
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 {
after: $normalizeChildren,
node,

View file

@ -62,7 +62,7 @@ describe('LexicalListItemNode tests', () => {
expectHtmlToBeEqual(
listItemNode.createDOM(editorConfig).outerHTML,
html`
<li value="1" class="my-listItem-item-class"></li>
<li value="1"></li>
`,
);
@ -90,7 +90,7 @@ describe('LexicalListItemNode tests', () => {
expectHtmlToBeEqual(
domElement.outerHTML,
html`
<li value="1" class="my-listItem-item-class"></li>
<li value="1"></li>
`,
);
const newListItemNode = new ListItemNode();
@ -106,7 +106,7 @@ describe('LexicalListItemNode tests', () => {
expectHtmlToBeEqual(
domElement.outerHTML,
html`
<li value="1" class="my-listItem-item-class"></li>
<li value="1"></li>
`,
);
});
@ -125,7 +125,7 @@ describe('LexicalListItemNode tests', () => {
expectHtmlToBeEqual(
domElement.outerHTML,
html`
<li value="1" class="my-listItem-item-class"></li>
<li value="1"></li>
`,
);
const nestedListNode = new ListNode('bullet', 1);
@ -142,7 +142,7 @@ describe('LexicalListItemNode tests', () => {
expectHtmlToBeEqual(
domElement.outerHTML,
html`
<li value="1" class="my-listItem-item-class my-nested-list-listItem-class"></li>
<li value="1" style="list-style: none;"></li>
`,
);
});
@ -486,53 +486,43 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
testEnv.innerHTML,
html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul>
<li value="1">
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">x</span>
</li>
<li value="2">
<span data-lexical-text="true">B</span>
</li>
</ul>
</div>
<ul>
<li value="1" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">x</span>
</li>
<li value="2">
<span data-lexical-text="true">B</span>
</li>
</ul>
`,
);
await editor.update(() => x.remove());
expectHtmlToBeEqual(
testEnv.outerHTML,
testEnv.innerHTML,
html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul>
<li value="1">
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">B</span>
</li>
</ul>
</div>
<ul>
<li value="1" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">B</span>
</li>
</ul>
`,
);
});
@ -566,53 +556,43 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
testEnv.innerHTML,
html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
<li value="2">
<span data-lexical-text="true">x</span>
</li>
<li value="3">
<ul>
<li value="1">
<span data-lexical-text="true">B</span>
</li>
</ul>
</li>
</ul>
</div>
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
<li value="2">
<span data-lexical-text="true">x</span>
</li>
<li value="3" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">B</span>
</li>
</ul>
</li>
</ul>
`,
);
await editor.update(() => x.remove());
expectHtmlToBeEqual(
testEnv.outerHTML,
testEnv.innerHTML,
html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
<li value="2">
<ul>
<li value="1">
<span data-lexical-text="true">B</span>
</li>
</ul>
</li>
</ul>
</div>
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
<li value="2" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">B</span>
</li>
</ul>
</li>
</ul>
`,
);
});
@ -650,57 +630,47 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
testEnv.innerHTML,
html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul>
<li value="1">
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">x</span>
</li>
<li value="2">
<ul>
<li value="1">
<span data-lexical-text="true">B</span>
</li>
</ul>
</li>
</ul>
</div>
<ul>
<li value="1" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">x</span>
</li>
<li value="2" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">B</span>
</li>
</ul>
</li>
</ul>
`,
);
await editor.update(() => x.remove());
expectHtmlToBeEqual(
testEnv.outerHTML,
testEnv.innerHTML,
html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul>
<li value="1">
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
<li value="2">
<span data-lexical-text="true">B</span>
</li>
</ul>
</li>
</ul>
</div>
<ul>
<li value="1" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
<li value="2">
<span data-lexical-text="true">B</span>
</li>
</ul>
</li>
</ul>
`,
);
});
@ -746,71 +716,61 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
testEnv.innerHTML,
html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul>
<li value="1">
<ul>
<li value="1">
<span data-lexical-text="true">A1</span>
</li>
<li value="2">
<ul>
<li value="1">
<span data-lexical-text="true">A2</span>
</li>
</ul>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">x</span>
</li>
<li value="2">
<ul>
<li value="1">
<span data-lexical-text="true">B</span>
</li>
</ul>
</li>
</ul>
</div>
<ul>
<li value="1" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">A1</span>
</li>
<li value="2" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">A2</span>
</li>
</ul>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">x</span>
</li>
<li value="2" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">B</span>
</li>
</ul>
</li>
</ul>
`,
);
await editor.update(() => x.remove());
expectHtmlToBeEqual(
testEnv.outerHTML,
testEnv.innerHTML,
html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul>
<li value="1">
<ul>
<li value="1">
<span data-lexical-text="true">A1</span>
</li>
<li value="2">
<ul>
<li value="1">
<span data-lexical-text="true">A2</span>
</li>
</ul>
</li>
<li value="2">
<span data-lexical-text="true">B</span>
</li>
</ul>
</li>
</ul>
</div>
<ul>
<li value="1" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">A1</span>
</li>
<li value="2" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">A2</span>
</li>
</ul>
</li>
<li value="2">
<span data-lexical-text="true">B</span>
</li>
</ul>
</li>
</ul>
`,
);
});
@ -856,71 +816,61 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
testEnv.innerHTML,
html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul>
<li value="1">
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">x</span>
</li>
<li value="2">
<ul>
<li value="1">
<ul>
<li value="1">
<span data-lexical-text="true">B1</span>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">B2</span>
</li>
</ul>
</li>
</ul>
</div>
<ul>
<li value="1" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">x</span>
</li>
<li value="2" style="list-style: none;">
<ul>
<li value="1" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">B1</span>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">B2</span>
</li>
</ul>
</li>
</ul>
`,
);
await editor.update(() => x.remove());
expectHtmlToBeEqual(
testEnv.outerHTML,
testEnv.innerHTML,
html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul>
<li value="1">
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
<li value="2">
<ul>
<li value="1">
<span data-lexical-text="true">B1</span>
</li>
</ul>
</li>
<li value="2">
<span data-lexical-text="true">B2</span>
</li>
</ul>
</li>
</ul>
</div>
<ul>
<li value="1" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">A</span>
</li>
<li value="2" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">B1</span>
</li>
</ul>
</li>
<li value="2">
<span data-lexical-text="true">B2</span>
</li>
</ul>
</li>
</ul>
`,
);
});
@ -974,81 +924,71 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
testEnv.innerHTML,
html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul>
<li value="1">
<ul>
<li value="1">
<span data-lexical-text="true">A1</span>
</li>
<li value="2">
<ul>
<li value="1">
<span data-lexical-text="true">A2</span>
</li>
</ul>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">x</span>
</li>
<li value="2">
<ul>
<li value="1">
<ul>
<li value="1">
<span data-lexical-text="true">B1</span>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">B2</span>
</li>
</ul>
</li>
</ul>
</div>
<ul>
<li value="1" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">A1</span>
</li>
<li value="2" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">A2</span>
</li>
</ul>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">x</span>
</li>
<li value="2" style="list-style: none;">
<ul>
<li value="1" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">B1</span>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">B2</span>
</li>
</ul>
</li>
</ul>
`,
);
await editor.update(() => x.remove());
expectHtmlToBeEqual(
testEnv.outerHTML,
testEnv.innerHTML,
html`
<div
contenteditable="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
data-lexical-editor="true">
<ul>
<li value="1">
<ul>
<li value="1">
<span data-lexical-text="true">A1</span>
</li>
<li value="2">
<ul>
<li value="1">
<span data-lexical-text="true">A2</span>
</li>
<li value="2">
<span data-lexical-text="true">B1</span>
</li>
</ul>
</li>
<li value="2">
<span data-lexical-text="true">B2</span>
</li>
</ul>
</li>
</ul>
</div>
<ul>
<li value="1" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">A1</span>
</li>
<li value="2" style="list-style: none;">
<ul>
<li value="1">
<span data-lexical-text="true">A2</span>
</li>
<li value="2">
<span data-lexical-text="true">B1</span>
</li>
</ul>
</li>
<li value="2">
<span data-lexical-text="true">B2</span>
</li>
</ul>
</li>
</ul>
`,
);
});
@ -1265,99 +1205,5 @@ describe('LexicalListItemNode tests', () => {
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');
});
});
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)) {
anchorNode.replace(list);
const listItem = $createListItemNode();
if ($isElementNode(anchorNode)) {
listItem.setFormat(anchorNode.getFormatType());
listItem.setIndent(anchorNode.getIndent());
}
list.append(listItem);
} else if ($isListItemNode(anchorNode)) {
const parent = anchorNode.getParentOrThrow();
@ -157,8 +153,6 @@ function $createListOrMerge(node: ElementNode, listType: ListType): ListNode {
const previousSibling = node.getPreviousSibling();
const nextSibling = node.getNextSibling();
const listItem = $createListItemNode();
listItem.setFormat(node.getFormatType());
listItem.setIndent(node.getIndent());
append(listItem, node.getChildren());
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 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 {
CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
SerializedCommonBlockNode,
setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps
} from "./_common";
} from "lexical/nodes/common";
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
export type CalloutCategory = 'info' | 'danger' | 'warning' | 'success';

View file

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

View file

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

View file

@ -8,8 +8,8 @@ import {
Spread
} from "lexical";
import type {EditorConfig} from "lexical/LexicalEditor";
import {EditorDecoratorAdapter} from "../ui/framework/decorator";
import {el} from "../utils/dom";
import {EditorDecoratorAdapter} from "../../ui/framework/decorator";
import {el} from "../../utils/dom";
export type SerializedDiagramNode = Spread<{
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
} from "lexical";
import type {EditorConfig} from "lexical/LexicalEditor";
import {CommonBlockAlignment, extractAlignmentFromElement} from "./_common";
import {$selectSingleNode} from "../utils/selection";
import {CommonBlockAlignment, extractAlignmentFromElement} from "lexical/nodes/common";
import {$selectSingleNode} from "../../utils/selection";
import {SerializedElementNode} from "lexical/nodes/LexicalElementNode";
export interface ImageNodeOptions {

View file

@ -8,14 +8,14 @@ import {
} from 'lexical';
import type {EditorConfig} from "lexical/LexicalEditor";
import {el, setOrRemoveAttribute, sizeToPixels} from "../utils/dom";
import {el, setOrRemoveAttribute, sizeToPixels} from "../../utils/dom";
import {
CommonBlockAlignment, deserializeCommonBlockNode,
SerializedCommonBlockNode,
setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps
} from "./_common";
import {$selectSingleNode} from "../utils/selection";
} from "lexical/nodes/common";
import {$selectSingleNode} from "../../utils/selection";
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';
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 {
$createTextNode,
$getRoot,
@ -19,6 +14,7 @@ import {
RangeSelection,
} from 'lexical';
import {initializeUnitTest} from 'lexical/__tests__/utils';
import {$createHeadingNode, $isHeadingNode, HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
const editorConfig = Object.freeze({
namespace: '',

View file

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

View file

@ -8,42 +8,14 @@
import type {
CommandPayloadType,
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
EditorConfig,
ElementFormatType,
LexicalCommand,
LexicalEditor,
LexicalNode,
NodeKey,
ParagraphNode,
PasteCommandType,
RangeSelection,
SerializedElementNode,
Spread,
TextFormatType,
} from 'lexical';
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,
$createTabNode,
$getAdjacentNode,
@ -55,7 +27,6 @@ import {
$isElementNode,
$isNodeSelection,
$isRangeSelection,
$isRootNode,
$isTextNode,
$normalizeSelection__EXPERIMENTAL,
$selectAll,
@ -75,7 +46,6 @@ import {
ElementNode,
FORMAT_ELEMENT_COMMAND,
FORMAT_TEXT_COMMAND,
INDENT_CONTENT_COMMAND,
INSERT_LINE_BREAK_COMMAND,
INSERT_PARAGRAPH_COMMAND,
INSERT_TAB_COMMAND,
@ -88,344 +58,22 @@ import {
KEY_DELETE_COMMAND,
KEY_ENTER_COMMAND,
KEY_ESCAPE_COMMAND,
OUTDENT_CONTENT_COMMAND,
PASTE_COMMAND,
REMOVE_TEXT_COMMAND,
SELECT_ALL_COMMAND,
} 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<
{
tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
},
SerializedElementNode
>;
import {$insertDataTransferForRichText, copyToClipboard,} from '@lexical/clipboard';
import {$moveCharacter, $shouldOverrideDefaultCharacterSelection,} from '@lexical/selection';
import {$findMatchingParent, mergeRegister, objectKlassEquals,} from '@lexical/utils';
import caretFromPoint from 'lexical/shared/caretFromPoint';
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(
'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(
event: CommandPayloadType<typeof PASTE_COMMAND>,
@ -651,9 +299,6 @@ export function registerRichText(editor: LexicalEditor): () => void {
(parentNode): parentNode is ElementNode =>
$isElementNode(parentNode) && !parentNode.isInline(),
);
if (element !== null) {
element.setFormat(format);
}
}
return true;
},
@ -691,28 +336,6 @@ export function registerRichText(editor: LexicalEditor): () => void {
},
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>(
KEY_ARROW_UP_COMMAND,
(event) => {
@ -846,19 +469,7 @@ export function registerRichText(editor: LexicalEditor): () => void {
return false;
}
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);
},
COMMAND_PRIORITY_EDITOR,

View file

@ -8,7 +8,7 @@
import {$createLinkNode} from '@lexical/link';
import {$createListItemNode, $createListNode} from '@lexical/list';
import {$createHeadingNode, registerRichText} from '@lexical/rich-text';
import {registerRichText} from '@lexical/rich-text';
import {
$addNodeStyle,
$getSelectionStyleValueForProperty,
@ -74,6 +74,7 @@ import {
} from '../utils';
import {createEmptyHistoryState, registerHistory} from "@lexical/history";
import {mergeRegister} from "@lexical/utils";
import {$createHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
interface ExpectedSelection {
anchorPath: number[];
@ -2604,7 +2605,7 @@ describe('LexicalSelection tests', () => {
return $createHeadingNode('h1');
});
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(
`<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(
`<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 {$createHeadingNode, $isHeadingNode} from '@lexical/rich-text';
import {
$getSelectionStyleValueForProperty,
$patchStyleText,
@ -44,6 +43,7 @@ import {
} from 'lexical/__tests__/utils';
import {$setAnchorPoint, $setFocusPoint} from '../utils';
import {$createHeadingNode, $isHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
Range.prototype.getBoundingClientRect = function (): DOMRect {
const rect = {

View file

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

View file

@ -28,7 +28,8 @@ import {
ElementNode,
} 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 = {
BOTH: 3,
@ -47,6 +48,8 @@ export type SerializedTableCellNode = Spread<
headerState: TableCellHeaderState;
width?: number;
backgroundColor?: null | string;
styles: Record<string, string>;
alignment: CommonBlockAlignment;
},
SerializedElementNode
>;
@ -63,6 +66,10 @@ export class TableCellNode extends ElementNode {
__width?: number;
/** @internal */
__backgroundColor: null | string;
/** @internal */
__styles: StyleMap = new Map;
/** @internal */
__alignment: CommonBlockAlignment = '';
static getType(): string {
return 'tablecell';
@ -77,6 +84,8 @@ export class TableCellNode extends ElementNode {
);
cellNode.__rowSpan = node.__rowSpan;
cellNode.__backgroundColor = node.__backgroundColor;
cellNode.__styles = new Map(node.__styles);
cellNode.__alignment = node.__alignment;
return cellNode;
}
@ -94,16 +103,20 @@ export class TableCellNode extends ElementNode {
}
static importJSON(serializedNode: SerializedTableCellNode): TableCellNode {
const colSpan = serializedNode.colSpan || 1;
const rowSpan = serializedNode.rowSpan || 1;
const cellNode = $createTableCellNode(
serializedNode.headerState,
colSpan,
serializedNode.width || undefined,
const node = $createTableCellNode(
serializedNode.headerState,
serializedNode.colSpan,
serializedNode.width,
);
cellNode.__rowSpan = rowSpan;
cellNode.__backgroundColor = serializedNode.backgroundColor || null;
return cellNode;
if (serializedNode.rowSpan) {
node.setRowSpan(serializedNode.rowSpan);
}
node.setStyles(new Map(Object.entries(serializedNode.styles)));
node.setAlignment(serializedNode.alignment);
return node;
}
constructor(
@ -144,34 +157,19 @@ export class TableCellNode extends ElementNode {
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;
}
exportDOM(editor: LexicalEditor): DOMExportOutput {
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 {
element,
};
@ -186,6 +184,8 @@ export class TableCellNode extends ElementNode {
rowSpan: this.__rowSpan,
type: 'tablecell',
width: this.getWidth(),
styles: Object.fromEntries(this.__styles),
alignment: this.__alignment,
};
}
@ -231,6 +231,38 @@ export class TableCellNode extends ElementNode {
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 {
return this.getLatest().__backgroundColor;
}
@ -265,7 +297,9 @@ export class TableCellNode extends ElementNode {
prevNode.__width !== this.__width ||
prevNode.__colSpan !== this.__colSpan ||
prevNode.__rowSpan !== this.__rowSpan ||
prevNode.__backgroundColor !== this.__backgroundColor
prevNode.__backgroundColor !== this.__backgroundColor ||
prevNode.__styles !== this.__styles ||
prevNode.__alignment !== this.__alignment
);
}
@ -287,38 +321,42 @@ export class TableCellNode extends ElementNode {
}
export function $convertTableCellNodeElement(
domNode: Node,
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,
nodeName === 'th'
? TableCellHeaderStates.ROW
: TableCellHeaderStates.NO_STATUS,
domNode_.colSpan,
width,
);
tableCellNode.__rowSpan = domNode_.rowSpan;
const backgroundColor = domNode_.style.backgroundColor;
if (backgroundColor !== '') {
tableCellNode.__backgroundColor = backgroundColor;
}
const style = domNode_.style;
const textDecoration = style.textDecoration.split(' ');
const hasBoldFontWeight =
style.fontWeight === '700' || style.fontWeight === 'bold';
style.fontWeight === '700' || style.fontWeight === 'bold';
const hasLinethroughTextDecoration = textDecoration.includes('line-through');
const hasItalicFontStyle = style.fontStyle === 'italic';
const hasUnderlineTextDecoration = textDecoration.includes('underline');
if (domNode instanceof HTMLElement) {
tableCellNode.setStyles(extractStyleMapFromElement(domNode));
tableCellNode.setAlignment(extractAlignmentFromElement(domNode));
}
return {
after: (childLexicalNodes) => {
if (childLexicalNodes.length === 0) {
@ -330,8 +368,8 @@ export function $convertTableCellNodeElement(
if ($isTableCellNode(parentLexicalNode) && !$isElementNode(lexicalNode)) {
const paragraphNode = $createParagraphNode();
if (
$isLineBreakNode(lexicalNode) &&
lexicalNode.getTextContent() === '\n'
$isLineBreakNode(lexicalNode) &&
lexicalNode.getTextContent() === '\n'
) {
return null;
}
@ -360,7 +398,7 @@ export function $convertTableCellNodeElement(
}
export function $createTableCellNode(
headerState: TableCellHeaderState,
headerState: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,
colSpan = 1,
width?: number,
): TableCellNode {

View file

@ -7,7 +7,7 @@
*/
import type {TableCellNode} from './LexicalTableCellNode';
import type {
import {
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
@ -15,31 +15,48 @@ import type {
LexicalEditor,
LexicalNode,
NodeKey,
SerializedElementNode,
Spread,
} from 'lexical';
import {addClassNamesToElement, isHTMLElement} from '@lexical/utils';
import {
$applyNodeReplacement,
$getNearestNodeFromDOMNode,
ElementNode,
} from 'lexical';
import {$isTableCellNode} from './LexicalTableCellNode';
import {TableDOMCell, TableDOMTable} from './LexicalTableObserver';
import {$isTableRowNode, TableRowNode} from './LexicalTableRowNode';
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 */
export class TableNode extends ElementNode {
export class TableNode extends CommonBlockNode {
__colWidths: string[] = [];
__styles: StyleMap = new Map;
static getType(): string {
return 'table';
}
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 {
@ -52,18 +69,24 @@ export class TableNode extends ElementNode {
}
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) {
super(key);
}
exportJSON(): SerializedElementNode {
exportJSON(): SerializedTableNode {
return {
...super.exportJSON(),
type: 'table',
version: 1,
colWidths: this.__colWidths,
styles: Object.fromEntries(this.__styles),
};
}
@ -72,11 +95,33 @@ export class TableNode extends ElementNode {
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;
}
updateDOM(): boolean {
return false;
updateDOM(_prevNode: TableNode): boolean {
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 {
@ -115,6 +160,26 @@ export class TableNode extends ElementNode {
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(
tableCellNode: TableCellNode,
table: TableDOMTable,
@ -239,8 +304,15 @@ export function $getElementForTableNode(
return getTable(tableElement);
}
export function $convertTableElement(_domNode: Node): DOMConversionOutput {
return {node: $createTableNode()};
export function $convertTableElement(element: HTMLElement): DOMConversionOutput {
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 {

View file

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

View file

@ -16,7 +16,6 @@ import type {
} from './LexicalTableSelection';
import type {
BaseSelection,
ElementFormatType,
LexicalCommand,
LexicalEditor,
LexicalNode,
@ -50,7 +49,6 @@ import {
DELETE_LINE_COMMAND,
DELETE_WORD_COMMAND,
FOCUS_COMMAND,
FORMAT_ELEMENT_COMMAND,
FORMAT_TEXT_COMMAND,
INSERT_PARAGRAPH_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(
editor.registerCommand(
CONTROLLED_TEXT_INSERTION_COMMAND,

View file

@ -113,9 +113,8 @@ describe('LexicalTableNode tests', () => {
$insertDataTransferForRichText(dataTransfer, selection, editor);
});
// Make sure paragraph is inserted inside empty cells
const emptyCell = '<td><p><br></p></td>';
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);
});
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>`,
);
const rowHeight = 36;
const rowWithCustomHeightNode = $createTableRowNode(36);
const rowWithCustomHeightNode = $createTableRowNode();
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,
__dir: null,
__first: paragraphKey,
__format: 0,
__indent: 0,
__key: 'root',
__last: paragraphKey,
__next: null,
@ -113,10 +111,11 @@ describe('table selection', () => {
__type: 'root',
});
expect(parsedParagraph).toEqual({
__alignment: "",
__dir: null,
__first: textKey,
__format: 0,
__indent: 0,
__id: '',
__inset: 0,
__key: paragraphKey,
__last: textKey,
__next: null,
@ -124,7 +123,6 @@ describe('table selection', () => {
__prev: null,
__size: 1,
__style: '',
__textFormat: 0,
__textStyle: '',
__type: 'paragraph',
});

View file

@ -7,7 +7,7 @@
*/
import {AutoLinkNode, LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list';
import {HeadingNode, QuoteNode, registerRichText} from '@lexical/rich-text';
import {registerRichText} from '@lexical/rich-text';
import {
applySelectionInputs,
pasteHTML,
@ -15,6 +15,8 @@ import {
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
import {$createParagraphNode, $insertNodes, LexicalEditor} from 'lexical';
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', () => {
const originalModule = jest.requireActual('lexical/shared/environment');
@ -174,7 +176,7 @@ describe('LexicalEventHelpers', () => {
},
{
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: [
pasteHTML(
`<meta charset='utf-8'><ul><li>Other side</li><li>I must have called</li></ul>`,
@ -184,7 +186,7 @@ describe('LexicalEventHelpers', () => {
},
{
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: [
pasteHTML(
`<meta charset='utf-8'><ol><li>To tell you</li><li>Im sorry</li></ol>`,
@ -264,7 +266,7 @@ describe('LexicalEventHelpers', () => {
},
{
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: [
pasteHTML(
`<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:
'<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: [
pasteHTML(
`<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:
'<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: [
pasteHTML('<ol><li>1<div></div>2</li><li></li><li>3</li></ol>'),
],
@ -645,7 +647,7 @@ describe('LexicalEventHelpers', () => {
},
{
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>')],
name: 'only br in a li',
},

View file

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

View file

@ -56,11 +56,11 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
expectedHtml:
'<ul>' +
'<li>Before</li>' +
'<li><ul><li>Hello</li></ul></li>' +
'<li style="list-style: none;"><ul><li>Hello</li></ul></li>' +
'</ul>' +
'<test-decorator></test-decorator>' +
'<ul>' +
'<li><ul><li>world</li></ul></li>' +
'<li style="list-style: none;"><ul><li>world</li></ul></li>' +
'<li>After</li>' +
'</ul>',
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 {
$createParagraphNode,
$insertNodes,
$isDecoratorNode, COMMAND_PRIORITY_HIGH, DROP_COMMAND,
LexicalEditor,
@ -7,8 +8,7 @@ import {
import {$insertNewBlockNodesAtSelection, $selectSingleNode} from "../utils/selection";
import {$getNearestBlockNodeForCoords, $htmlToBlockNodes} from "../utils/nodes";
import {Clipboard} from "../../services/clipboard";
import {$createImageNode} from "../nodes/image";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
import {$createImageNode} from "@lexical/rich-text/LexicalImageNode";
import {$createLinkNode} from "@lexical/link";
import {EditorImageData, uploadImageFile} from "../utils/images";
import {EditorUiContext} from "../ui/framework/core";
@ -67,7 +67,7 @@ function handleMediaInsert(data: DataTransfer, context: EditorUiContext): boolea
for (const imageFile of images) {
const loadingImage = window.baseUrl('/loading.gif');
const loadingNode = $createImageNode(loadingImage);
const imageWrap = $createCustomParagraphNode();
const imageWrap = $createParagraphNode();
imageWrap.append(loadingNode);
$insertNodes([imageWrap]);

View file

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

View file

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

View file

@ -2,7 +2,11 @@
## 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

View file

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

View file

@ -1,7 +1,7 @@
import {EditorDecorator} from "../framework/decorator";
import {EditorUiContext} from "../framework/core";
import {BaseSelection} from "lexical";
import {DiagramNode} from "../../nodes/diagram";
import {DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection";
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 {
$getBlockElementNodesInSelection,
$selectionContainsAlignment, $selectionContainsDirection, $selectSingleNode, $toggleSelection, getLastSelection
$selectionContainsAlignment, $selectionContainsDirection, $selectSingleNode, getLastSelection
} from "../../../utils/selection";
import {CommonBlockAlignment} from "../../../nodes/_common";
import {CommonBlockAlignment} from "lexical/nodes/common";
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 {EditorUiContext} from "../../framework/core";
import {$isParagraphNode, BaseSelection, LexicalNode} from "lexical";
import {
$isHeadingNode,
$isQuoteNode,
HeadingNode,
HeadingTagType
} from "@lexical/rich-text";
import {$selectionContainsNodeType, $toggleSelectionBlockNodeType} from "../../../utils/selection";
import {
toggleSelectionAsBlockquote,
toggleSelectionAsHeading,
toggleSelectionAsParagraph
} 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 {
return {

View file

@ -2,27 +2,26 @@ import {EditorButtonDefinition} from "../../framework/buttons";
import linkIcon from "@icons/editor/link.svg";
import {EditorUiContext} from "../../framework/core";
import {
$createTextNode,
$getRoot,
$getSelection, $insertNodes,
BaseSelection,
ElementNode, isCurrentlyReadOnlyMode
ElementNode
} from "lexical";
import {$isLinkNode, LinkNode} from "@lexical/link";
import unlinkIcon from "@icons/editor/unlink.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 {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../../nodes/horizontal-rule";
import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "@lexical/rich-text/LexicalHorizontalRuleNode";
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 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 mediaIcon from "@icons/editor/media.svg";
import {$createDetailsNode, $isDetailsNode} from "../../../nodes/details";
import {$isMediaNode, MediaNode} from "../../../nodes/media";
import {$createDetailsNode, $isDetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
import {$isMediaNode, MediaNode} from "@lexical/rich-text/LexicalMediaNode";
import {
$getNodeFromSelection,
$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 {EditorUiContext} from "../../framework/core";
import {$getSelection, BaseSelection} from "lexical";
import {$isCustomTableNode} from "../../../nodes/custom-table";
import {
$deleteTableColumn__EXPERIMENTAL,
$deleteTableRow__EXPERIMENTAL,
$insertTableColumn__EXPERIMENTAL,
$insertTableRow__EXPERIMENTAL,
$isTableNode, $isTableSelection, $unmergeCell, TableCellNode,
$insertTableRow__EXPERIMENTAL, $isTableCellNode,
$isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode,
} from "@lexical/table";
import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/selection";
import {$getParentOfType} from "../../../utils/nodes";
import {$isCustomTableCellNode} from "../../../nodes/custom-table-cell";
import {$showCellPropertiesForm, $showRowPropertiesForm, $showTablePropertiesForm} from "../forms/tables";
import {
$clearTableFormatting,
@ -27,7 +25,6 @@ import {
$getTableRowsFromSelection,
$mergeTableCellsInSelection
} from "../../../utils/tables";
import {$isCustomTableRowNode} from "../../../nodes/custom-table-row";
import {
$copySelectedColumnsToClipboard,
$copySelectedRowsToClipboard,
@ -41,7 +38,7 @@ import {
} from "../../../utils/table-copy-paste";
const neverActive = (): boolean => false;
const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode);
const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isTableCellNode);
export const table: EditorBasicButtonDefinition = {
label: 'Table',
@ -54,7 +51,7 @@ export const tableProperties: EditorButtonDefinition = {
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
const table = $getTableFromSelection($getSelection());
if ($isCustomTableNode(table)) {
if ($isTableNode(table)) {
$showTablePropertiesForm(table, context);
}
});
@ -68,13 +65,13 @@ export const clearTableFormatting: EditorButtonDefinition = {
format: 'long',
action(context: EditorUiContext) {
context.editor.update(() => {
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
if (!$isCustomTableCellNode(cell)) {
const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
if (!$isTableCellNode(cell)) {
return;
}
const table = $getParentOfType(cell, $isTableNode);
if ($isCustomTableNode(table)) {
if ($isTableNode(table)) {
$clearTableFormatting(table);
}
});
@ -88,13 +85,13 @@ export const resizeTableToContents: EditorButtonDefinition = {
format: 'long',
action(context: EditorUiContext) {
context.editor.update(() => {
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
if (!$isCustomTableCellNode(cell)) {
const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
if (!$isTableCellNode(cell)) {
return;
}
const table = $getParentOfType(cell, $isCustomTableNode);
if ($isCustomTableNode(table)) {
const table = $getParentOfType(cell, $isTableNode);
if ($isTableNode(table)) {
$clearTableSizes(table);
}
});
@ -108,7 +105,7 @@ export const deleteTable: EditorButtonDefinition = {
icon: deleteIcon,
action(context: EditorUiContext) {
context.editor.update(() => {
const table = $getNodeFromSelection($getSelection(), $isCustomTableNode);
const table = $getNodeFromSelection($getSelection(), $isTableNode);
if (table) {
table.remove();
}
@ -169,7 +166,7 @@ export const rowProperties: EditorButtonDefinition = {
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
const rows = $getTableRowsFromSelection($getSelection());
if ($isCustomTableRowNode(rows[0])) {
if ($isTableRowNode(rows[0])) {
$showRowPropertiesForm(rows[0], context);
}
});
@ -350,8 +347,8 @@ export const cellProperties: EditorButtonDefinition = {
format: 'long',
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
if ($isCustomTableCellNode(cell)) {
const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
if ($isTableCellNode(cell)) {
$showCellPropertiesForm(cell, context);
}
});
@ -387,7 +384,7 @@ export const splitCell: EditorButtonDefinition = {
},
isActive: neverActive,
isDisabled(selection) {
const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as TableCellNode|null;
const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode|null;
if (cell) {
const merged = cell.getRowSpan() > 1 || cell.getColSpan() > 1;
return !merged;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import {EditorFormModal, EditorFormModalDefinition} from "./modals";
import {EditorContainerUiElement, EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core";
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 type {NodeKey} from "lexical/LexicalNode";
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 {EditorUiContext} from "../ui/framework/core";
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 {EditorImageData} from "./images";
import {$getNodeFromSelection, getLastSelection} from "./selection";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,4 @@
import {CustomTableNode} from "../nodes/custom-table";
import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell";
import {$isTableRowNode} from "@lexical/table";
import {$isTableCellNode, $isTableRowNode, TableCellNode, TableNode} from "@lexical/table";
export type CellRange = {
fromX: number;
@ -16,15 +14,15 @@ export class TableMap {
// 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.
cells: CustomTableCellNode[] = [];
cells: TableCellNode[] = [];
constructor(table: CustomTableNode) {
constructor(table: TableNode) {
this.buildCellMap(table);
}
protected buildCellMap(table: CustomTableNode) {
const rowsAndCells: CustomTableCellNode[][] = [];
const setCell = (x: number, y: number, cell: CustomTableCellNode) => {
protected buildCellMap(table: TableNode) {
const rowsAndCells: TableCellNode[][] = [];
const setCell = (x: number, y: number, cell: TableCellNode) => {
if (typeof rowsAndCells[y] === 'undefined') {
rowsAndCells[y] = [];
}
@ -36,7 +34,7 @@ export class TableMap {
const rowNodes = table.getChildren().filter(r => $isTableRowNode(r));
for (let rowIndex = 0; rowIndex < rowNodes.length; 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;
for (let cellIndex = 0; cellIndex < cellNodes.length; cellIndex++) {
const cellNode = cellNodes[cellIndex];
@ -60,7 +58,7 @@ export class TableMap {
this.columnCount = Math.max(...rowsAndCells.map(r => r.length));
const cells = [];
let lastCell: CustomTableCellNode = rowsAndCells[0][0];
let lastCell: TableCellNode = rowsAndCells[0][0];
for (let y = 0; y < this.rowCount; y++) {
for (let x = 0; x < this.columnCount; x++) {
if (!rowsAndCells[y] || !rowsAndCells[y][x]) {
@ -75,7 +73,7 @@ export class TableMap {
this.cells = cells;
}
public getCellAtPosition(x: number, y: number): CustomTableCellNode {
public getCellAtPosition(x: number, y: number): TableCellNode {
const position = (y * this.columnCount) + x;
if (position >= 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];
}
public getCellsInRange(range: CellRange): CustomTableCellNode[] {
public getCellsInRange(range: CellRange): TableCellNode[] {
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 minY = Math.max(Math.min(range.fromY, range.toY), 0);
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 x = minX; x <= maxX; x++) {
@ -101,7 +99,7 @@ export class TableMap {
return [...cells.values()];
}
public getCellsInColumn(columnIndex: number): CustomTableCellNode[] {
public getCellsInColumn(columnIndex: number): TableCellNode[] {
return this.getCellsInRange({
fromX: 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;
const cellKey = cell.getKey();

View file

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