Merge pull request #5349 from BookStackApp/lexical_reorg
Lexical: Merge of custom nodes & re-organisation of codebase
This commit is contained in:
commit
7e6f6af463
|
@ -1,4 +1,4 @@
|
||||||
import {$getSelection, createEditor, CreateEditorArgs, isCurrentlyReadOnlyMode, LexicalEditor} from 'lexical';
|
import {$getSelection, createEditor, CreateEditorArgs, LexicalEditor} from 'lexical';
|
||||||
import {createEmptyHistoryState, registerHistory} from '@lexical/history';
|
import {createEmptyHistoryState, registerHistory} from '@lexical/history';
|
||||||
import {registerRichText} from '@lexical/rich-text';
|
import {registerRichText} from '@lexical/rich-text';
|
||||||
import {mergeRegister} from '@lexical/utils';
|
import {mergeRegister} from '@lexical/utils';
|
||||||
|
|
|
@ -355,7 +355,6 @@ function onSelectionChange(
|
||||||
lastNode instanceof ParagraphNode &&
|
lastNode instanceof ParagraphNode &&
|
||||||
lastNode.getChildrenSize() === 0
|
lastNode.getChildrenSize() === 0
|
||||||
) {
|
) {
|
||||||
selection.format = lastNode.getTextFormat();
|
|
||||||
selection.style = lastNode.getTextStyle();
|
selection.style = lastNode.getTextStyle();
|
||||||
} else {
|
} else {
|
||||||
selection.format = 0;
|
selection.format = 0;
|
||||||
|
@ -578,7 +577,6 @@ function onBeforeInput(event: InputEvent, editor: LexicalEditor): void {
|
||||||
if ($isRangeSelection(selection)) {
|
if ($isRangeSelection(selection)) {
|
||||||
const anchorNode = selection.anchor.getNode();
|
const anchorNode = selection.anchor.getNode();
|
||||||
anchorNode.markDirty();
|
anchorNode.markDirty();
|
||||||
selection.format = anchorNode.getFormat();
|
|
||||||
invariant(
|
invariant(
|
||||||
$isTextNode(anchorNode),
|
$isTextNode(anchorNode),
|
||||||
'Anchor node must be a TextNode',
|
'Anchor node must be a TextNode',
|
||||||
|
@ -912,7 +910,6 @@ function onCompositionStart(
|
||||||
// need to invoke the empty space heuristic below.
|
// need to invoke the empty space heuristic below.
|
||||||
anchor.type === 'element' ||
|
anchor.type === 'element' ||
|
||||||
!selection.isCollapsed() ||
|
!selection.isCollapsed() ||
|
||||||
node.getFormat() !== selection.format ||
|
|
||||||
($isTextNode(node) && node.getStyle() !== selection.style)
|
($isTextNode(node) && node.getStyle() !== selection.style)
|
||||||
) {
|
) {
|
||||||
// We insert a zero width character, ready for the composition
|
// We insert a zero width character, ready for the composition
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
$getSelection,
|
$getSelection,
|
||||||
$isDecoratorNode,
|
$isDecoratorNode,
|
||||||
$isElementNode,
|
$isElementNode,
|
||||||
$isRangeSelection,
|
|
||||||
$isTextNode,
|
$isTextNode,
|
||||||
$setSelection,
|
$setSelection,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
@ -96,15 +95,6 @@ function shouldUpdateTextNodeFromMutation(
|
||||||
targetDOM: Node,
|
targetDOM: Node,
|
||||||
targetNode: TextNode,
|
targetNode: TextNode,
|
||||||
): boolean {
|
): boolean {
|
||||||
if ($isRangeSelection(selection)) {
|
|
||||||
const anchorNode = selection.anchor.getNode();
|
|
||||||
if (
|
|
||||||
anchorNode.is(targetNode) &&
|
|
||||||
selection.format !== anchorNode.getFormat()
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return targetDOM.nodeType === DOM_TEXT_TYPE && targetNode.isAttached();
|
return targetDOM.nodeType === DOM_TEXT_TYPE && targetNode.isAttached();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ import type {NodeKey, NodeMap} from './LexicalNode';
|
||||||
import type {ElementNode} from './nodes/LexicalElementNode';
|
import type {ElementNode} from './nodes/LexicalElementNode';
|
||||||
|
|
||||||
import invariant from 'lexical/shared/invariant';
|
import invariant from 'lexical/shared/invariant';
|
||||||
import normalizeClassNames from 'lexical/shared/normalizeClassNames';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
$isDecoratorNode,
|
$isDecoratorNode,
|
||||||
|
@ -30,12 +29,12 @@ import {
|
||||||
import {
|
import {
|
||||||
DOUBLE_LINE_BREAK,
|
DOUBLE_LINE_BREAK,
|
||||||
FULL_RECONCILE,
|
FULL_RECONCILE,
|
||||||
IS_ALIGN_CENTER,
|
|
||||||
IS_ALIGN_END,
|
|
||||||
IS_ALIGN_JUSTIFY,
|
|
||||||
IS_ALIGN_LEFT,
|
|
||||||
IS_ALIGN_RIGHT,
|
|
||||||
IS_ALIGN_START,
|
|
||||||
} from './LexicalConstants';
|
} from './LexicalConstants';
|
||||||
import {EditorState} from './LexicalEditorState';
|
import {EditorState} from './LexicalEditorState';
|
||||||
import {
|
import {
|
||||||
|
@ -117,51 +116,6 @@ function setTextAlign(domStyle: CSSStyleDeclaration, value: string): void {
|
||||||
domStyle.setProperty('text-align', value);
|
domStyle.setProperty('text-align', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_INDENT_VALUE = '40px';
|
|
||||||
|
|
||||||
function setElementIndent(dom: HTMLElement, indent: number): void {
|
|
||||||
const indentClassName = activeEditorConfig.theme.indent;
|
|
||||||
|
|
||||||
if (typeof indentClassName === 'string') {
|
|
||||||
const elementHasClassName = dom.classList.contains(indentClassName);
|
|
||||||
|
|
||||||
if (indent > 0 && !elementHasClassName) {
|
|
||||||
dom.classList.add(indentClassName);
|
|
||||||
} else if (indent < 1 && elementHasClassName) {
|
|
||||||
dom.classList.remove(indentClassName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const indentationBaseValue =
|
|
||||||
getComputedStyle(dom).getPropertyValue('--lexical-indent-base-value') ||
|
|
||||||
DEFAULT_INDENT_VALUE;
|
|
||||||
|
|
||||||
dom.style.setProperty(
|
|
||||||
'padding-inline-start',
|
|
||||||
indent === 0 ? '' : `calc(${indent} * ${indentationBaseValue})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setElementFormat(dom: HTMLElement, format: number): void {
|
|
||||||
const domStyle = dom.style;
|
|
||||||
|
|
||||||
if (format === 0) {
|
|
||||||
setTextAlign(domStyle, '');
|
|
||||||
} else if (format === IS_ALIGN_LEFT) {
|
|
||||||
setTextAlign(domStyle, 'left');
|
|
||||||
} else if (format === IS_ALIGN_CENTER) {
|
|
||||||
setTextAlign(domStyle, 'center');
|
|
||||||
} else if (format === IS_ALIGN_RIGHT) {
|
|
||||||
setTextAlign(domStyle, 'right');
|
|
||||||
} else if (format === IS_ALIGN_JUSTIFY) {
|
|
||||||
setTextAlign(domStyle, 'justify');
|
|
||||||
} else if (format === IS_ALIGN_START) {
|
|
||||||
setTextAlign(domStyle, 'start');
|
|
||||||
} else if (format === IS_ALIGN_END) {
|
|
||||||
setTextAlign(domStyle, 'end');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function $createNode(
|
function $createNode(
|
||||||
key: NodeKey,
|
key: NodeKey,
|
||||||
parentDOM: null | HTMLElement,
|
parentDOM: null | HTMLElement,
|
||||||
|
@ -185,22 +139,14 @@ function $createNode(
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isElementNode(node)) {
|
if ($isElementNode(node)) {
|
||||||
const indent = node.__indent;
|
|
||||||
const childrenSize = node.__size;
|
const childrenSize = node.__size;
|
||||||
|
|
||||||
if (indent !== 0) {
|
|
||||||
setElementIndent(dom, indent);
|
|
||||||
}
|
|
||||||
if (childrenSize !== 0) {
|
if (childrenSize !== 0) {
|
||||||
const endIndex = childrenSize - 1;
|
const endIndex = childrenSize - 1;
|
||||||
const children = createChildrenArray(node, activeNextNodeMap);
|
const children = createChildrenArray(node, activeNextNodeMap);
|
||||||
$createChildren(children, node, 0, endIndex, dom, null);
|
$createChildren(children, node, 0, endIndex, dom, null);
|
||||||
}
|
}
|
||||||
const format = node.__format;
|
|
||||||
|
|
||||||
if (format !== 0) {
|
|
||||||
setElementFormat(dom, format);
|
|
||||||
}
|
|
||||||
if (!node.isInline()) {
|
if (!node.isInline()) {
|
||||||
reconcileElementTerminatingLineBreak(null, node, dom);
|
reconcileElementTerminatingLineBreak(null, node, dom);
|
||||||
}
|
}
|
||||||
|
@ -349,10 +295,8 @@ function reconcileParagraphFormat(element: ElementNode): void {
|
||||||
if (
|
if (
|
||||||
$isParagraphNode(element) &&
|
$isParagraphNode(element) &&
|
||||||
subTreeTextFormat != null &&
|
subTreeTextFormat != null &&
|
||||||
subTreeTextFormat !== element.__textFormat &&
|
|
||||||
!activeEditorStateReadOnly
|
!activeEditorStateReadOnly
|
||||||
) {
|
) {
|
||||||
element.setTextFormat(subTreeTextFormat);
|
|
||||||
element.setTextStyle(subTreeTextStyle);
|
element.setTextStyle(subTreeTextStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -563,17 +507,6 @@ function $reconcileNode(
|
||||||
|
|
||||||
if ($isElementNode(prevNode) && $isElementNode(nextNode)) {
|
if ($isElementNode(prevNode) && $isElementNode(nextNode)) {
|
||||||
// Reconcile element children
|
// Reconcile element children
|
||||||
const nextIndent = nextNode.__indent;
|
|
||||||
|
|
||||||
if (nextIndent !== prevNode.__indent) {
|
|
||||||
setElementIndent(dom, nextIndent);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextFormat = nextNode.__format;
|
|
||||||
|
|
||||||
if (nextFormat !== prevNode.__format) {
|
|
||||||
setElementFormat(dom, nextFormat);
|
|
||||||
}
|
|
||||||
if (isDirty) {
|
if (isDirty) {
|
||||||
$reconcileChildrenWithDirection(prevNode, nextNode, dom);
|
$reconcileChildrenWithDirection(prevNode, nextNode, dom);
|
||||||
if (!$isRootNode(nextNode) && !nextNode.isInline()) {
|
if (!$isRootNode(nextNode) && !nextNode.isInline()) {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -47,7 +47,6 @@ import {
|
||||||
import invariant from 'lexical/shared/invariant';
|
import invariant from 'lexical/shared/invariant';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
$createTestDecoratorNode,
|
|
||||||
$createTestElementNode,
|
$createTestElementNode,
|
||||||
$createTestInlineElementNode,
|
$createTestInlineElementNode,
|
||||||
createTestEditor,
|
createTestEditor,
|
||||||
|
@ -975,7 +974,7 @@ describe('LexicalEditor tests', () => {
|
||||||
editable ? 'editable' : 'non-editable'
|
editable ? 'editable' : 'non-editable'
|
||||||
})`, async () => {
|
})`, async () => {
|
||||||
const JSON_EDITOR_STATE =
|
const JSON_EDITOR_STATE =
|
||||||
'{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"123","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"root","version":1}}';
|
'{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"123","type":"text","version":1}],"direction":null,"type":"paragraph","version":1,"textStyle":""}],"direction":null,"type":"root","version":1}}';
|
||||||
init();
|
init();
|
||||||
const contentEditable = editor.getRootElement();
|
const contentEditable = editor.getRootElement();
|
||||||
editor.setEditable(editable);
|
editor.setEditable(editable);
|
||||||
|
@ -1048,8 +1047,6 @@ describe('LexicalEditor tests', () => {
|
||||||
__cachedText: null,
|
__cachedText: null,
|
||||||
__dir: null,
|
__dir: null,
|
||||||
__first: paragraphKey,
|
__first: paragraphKey,
|
||||||
__format: 0,
|
|
||||||
__indent: 0,
|
|
||||||
__key: 'root',
|
__key: 'root',
|
||||||
__last: paragraphKey,
|
__last: paragraphKey,
|
||||||
__next: null,
|
__next: null,
|
||||||
|
@ -1060,10 +1057,11 @@ describe('LexicalEditor tests', () => {
|
||||||
__type: 'root',
|
__type: 'root',
|
||||||
});
|
});
|
||||||
expect(parsedParagraph).toEqual({
|
expect(parsedParagraph).toEqual({
|
||||||
|
"__alignment": "",
|
||||||
__dir: null,
|
__dir: null,
|
||||||
__first: textKey,
|
__first: textKey,
|
||||||
__format: 0,
|
__id: '',
|
||||||
__indent: 0,
|
__inset: 0,
|
||||||
__key: paragraphKey,
|
__key: paragraphKey,
|
||||||
__last: textKey,
|
__last: textKey,
|
||||||
__next: null,
|
__next: null,
|
||||||
|
@ -1071,7 +1069,6 @@ describe('LexicalEditor tests', () => {
|
||||||
__prev: null,
|
__prev: null,
|
||||||
__size: 1,
|
__size: 1,
|
||||||
__style: '',
|
__style: '',
|
||||||
__textFormat: 0,
|
|
||||||
__textStyle: '',
|
__textStyle: '',
|
||||||
__type: 'paragraph',
|
__type: 'paragraph',
|
||||||
});
|
});
|
||||||
|
@ -1130,8 +1127,6 @@ describe('LexicalEditor tests', () => {
|
||||||
__cachedText: null,
|
__cachedText: null,
|
||||||
__dir: null,
|
__dir: null,
|
||||||
__first: paragraphKey,
|
__first: paragraphKey,
|
||||||
__format: 0,
|
|
||||||
__indent: 0,
|
|
||||||
__key: 'root',
|
__key: 'root',
|
||||||
__last: paragraphKey,
|
__last: paragraphKey,
|
||||||
__next: null,
|
__next: null,
|
||||||
|
@ -1142,10 +1137,11 @@ describe('LexicalEditor tests', () => {
|
||||||
__type: 'root',
|
__type: 'root',
|
||||||
});
|
});
|
||||||
expect(parsedParagraph).toEqual({
|
expect(parsedParagraph).toEqual({
|
||||||
|
"__alignment": "",
|
||||||
__dir: null,
|
__dir: null,
|
||||||
__first: textKey,
|
__first: textKey,
|
||||||
__format: 0,
|
__id: '',
|
||||||
__indent: 0,
|
__inset: 0,
|
||||||
__key: paragraphKey,
|
__key: paragraphKey,
|
||||||
__last: textKey,
|
__last: textKey,
|
||||||
__next: null,
|
__next: null,
|
||||||
|
@ -1153,7 +1149,6 @@ describe('LexicalEditor tests', () => {
|
||||||
__prev: null,
|
__prev: null,
|
||||||
__size: 1,
|
__size: 1,
|
||||||
__style: '',
|
__style: '',
|
||||||
__textFormat: 0,
|
|
||||||
__textStyle: '',
|
__textStyle: '',
|
||||||
__type: 'paragraph',
|
__type: 'paragraph',
|
||||||
});
|
});
|
||||||
|
|
|
@ -54,8 +54,6 @@ describe('LexicalEditorState tests', () => {
|
||||||
__cachedText: 'foo',
|
__cachedText: 'foo',
|
||||||
__dir: null,
|
__dir: null,
|
||||||
__first: '1',
|
__first: '1',
|
||||||
__format: 0,
|
|
||||||
__indent: 0,
|
|
||||||
__key: 'root',
|
__key: 'root',
|
||||||
__last: '1',
|
__last: '1',
|
||||||
__next: null,
|
__next: null,
|
||||||
|
@ -66,10 +64,11 @@ describe('LexicalEditorState tests', () => {
|
||||||
__type: 'root',
|
__type: 'root',
|
||||||
});
|
});
|
||||||
expect(paragraph).toEqual({
|
expect(paragraph).toEqual({
|
||||||
|
"__alignment": "",
|
||||||
__dir: null,
|
__dir: null,
|
||||||
__first: '2',
|
__first: '2',
|
||||||
__format: 0,
|
__id: '',
|
||||||
__indent: 0,
|
__inset: 0,
|
||||||
__key: '1',
|
__key: '1',
|
||||||
__last: '2',
|
__last: '2',
|
||||||
__next: null,
|
__next: null,
|
||||||
|
@ -77,7 +76,6 @@ describe('LexicalEditorState tests', () => {
|
||||||
__prev: null,
|
__prev: null,
|
||||||
__size: 1,
|
__size: 1,
|
||||||
__style: '',
|
__style: '',
|
||||||
__textFormat: 0,
|
|
||||||
__textStyle: '',
|
__textStyle: '',
|
||||||
__type: 'paragraph',
|
__type: 'paragraph',
|
||||||
});
|
});
|
||||||
|
@ -113,7 +111,7 @@ describe('LexicalEditorState tests', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(JSON.stringify(editor.getEditorState().toJSON())).toEqual(
|
expect(JSON.stringify(editor.getEditorState().toJSON())).toEqual(
|
||||||
`{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Hello world","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"root","version":1}}`,
|
`{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Hello world","type":"text","version":1}],"direction":null,"type":"paragraph","version":1,"id":"","alignment":"","inset":0,"textStyle":""}],"direction":null,"type":"root","version":1}}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -140,8 +138,6 @@ describe('LexicalEditorState tests', () => {
|
||||||
__cachedText: '',
|
__cachedText: '',
|
||||||
__dir: null,
|
__dir: null,
|
||||||
__first: null,
|
__first: null,
|
||||||
__format: 0,
|
|
||||||
__indent: 0,
|
|
||||||
__key: 'root',
|
__key: 'root',
|
||||||
__last: null,
|
__last: null,
|
||||||
__next: null,
|
__next: null,
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -10,7 +10,6 @@ import {createHeadlessEditor} from '@lexical/headless';
|
||||||
import {AutoLinkNode, LinkNode} from '@lexical/link';
|
import {AutoLinkNode, LinkNode} from '@lexical/link';
|
||||||
import {ListItemNode, ListNode} from '@lexical/list';
|
import {ListItemNode, ListNode} from '@lexical/list';
|
||||||
|
|
||||||
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
|
|
||||||
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
|
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -36,6 +35,8 @@ import {
|
||||||
LexicalNodeReplacement,
|
LexicalNodeReplacement,
|
||||||
} from '../../LexicalEditor';
|
} from '../../LexicalEditor';
|
||||||
import {resetRandomKey} from '../../LexicalUtils';
|
import {resetRandomKey} from '../../LexicalUtils';
|
||||||
|
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
|
||||||
|
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
|
||||||
|
|
||||||
|
|
||||||
type TestEnv = {
|
type TestEnv = {
|
||||||
|
@ -129,8 +130,6 @@ export class TestElementNode extends ElementNode {
|
||||||
serializedNode: SerializedTestElementNode,
|
serializedNode: SerializedTestElementNode,
|
||||||
): TestInlineElementNode {
|
): TestInlineElementNode {
|
||||||
const node = $createTestInlineElementNode();
|
const node = $createTestInlineElementNode();
|
||||||
node.setFormat(serializedNode.format);
|
|
||||||
node.setIndent(serializedNode.indent);
|
|
||||||
node.setDirection(serializedNode.direction);
|
node.setDirection(serializedNode.direction);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
@ -195,8 +194,6 @@ export class TestInlineElementNode extends ElementNode {
|
||||||
serializedNode: SerializedTestInlineElementNode,
|
serializedNode: SerializedTestInlineElementNode,
|
||||||
): TestInlineElementNode {
|
): TestInlineElementNode {
|
||||||
const node = $createTestInlineElementNode();
|
const node = $createTestInlineElementNode();
|
||||||
node.setFormat(serializedNode.format);
|
|
||||||
node.setIndent(serializedNode.indent);
|
|
||||||
node.setDirection(serializedNode.direction);
|
node.setDirection(serializedNode.direction);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
@ -241,8 +238,6 @@ export class TestShadowRootNode extends ElementNode {
|
||||||
serializedNode: SerializedTestShadowRootNode,
|
serializedNode: SerializedTestShadowRootNode,
|
||||||
): TestShadowRootNode {
|
): TestShadowRootNode {
|
||||||
const node = $createTestShadowRootNode();
|
const node = $createTestShadowRootNode();
|
||||||
node.setFormat(serializedNode.format);
|
|
||||||
node.setIndent(serializedNode.indent);
|
|
||||||
node.setDirection(serializedNode.direction);
|
node.setDirection(serializedNode.direction);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
@ -322,8 +317,6 @@ export class TestExcludeFromCopyElementNode extends ElementNode {
|
||||||
serializedNode: SerializedTestExcludeFromCopyElementNode,
|
serializedNode: SerializedTestExcludeFromCopyElementNode,
|
||||||
): TestExcludeFromCopyElementNode {
|
): TestExcludeFromCopyElementNode {
|
||||||
const node = $createTestExcludeFromCopyElementNode();
|
const node = $createTestExcludeFromCopyElementNode();
|
||||||
node.setFormat(serializedNode.format);
|
|
||||||
node.setIndent(serializedNode.indent);
|
|
||||||
node.setDirection(serializedNode.direction);
|
node.setDirection(serializedNode.direction);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -19,8 +19,8 @@ import invariant from 'lexical/shared/invariant';
|
||||||
import {$isTextNode, TextNode} from '../index';
|
import {$isTextNode, TextNode} from '../index';
|
||||||
import {
|
import {
|
||||||
DOUBLE_LINE_BREAK,
|
DOUBLE_LINE_BREAK,
|
||||||
ELEMENT_FORMAT_TO_TYPE,
|
|
||||||
ELEMENT_TYPE_TO_FORMAT,
|
|
||||||
} from '../LexicalConstants';
|
} from '../LexicalConstants';
|
||||||
import {LexicalNode} from '../LexicalNode';
|
import {LexicalNode} from '../LexicalNode';
|
||||||
import {
|
import {
|
||||||
|
@ -42,8 +42,6 @@ export type SerializedElementNode<
|
||||||
{
|
{
|
||||||
children: Array<T>;
|
children: Array<T>;
|
||||||
direction: 'ltr' | 'rtl' | null;
|
direction: 'ltr' | 'rtl' | null;
|
||||||
format: ElementFormatType;
|
|
||||||
indent: number;
|
|
||||||
},
|
},
|
||||||
SerializedLexicalNode
|
SerializedLexicalNode
|
||||||
>;
|
>;
|
||||||
|
@ -74,12 +72,8 @@ export class ElementNode extends LexicalNode {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
__size: number;
|
__size: number;
|
||||||
/** @internal */
|
/** @internal */
|
||||||
__format: number;
|
|
||||||
/** @internal */
|
|
||||||
__style: string;
|
__style: string;
|
||||||
/** @internal */
|
/** @internal */
|
||||||
__indent: number;
|
|
||||||
/** @internal */
|
|
||||||
__dir: 'ltr' | 'rtl' | null;
|
__dir: 'ltr' | 'rtl' | null;
|
||||||
|
|
||||||
constructor(key?: NodeKey) {
|
constructor(key?: NodeKey) {
|
||||||
|
@ -87,9 +81,7 @@ export class ElementNode extends LexicalNode {
|
||||||
this.__first = null;
|
this.__first = null;
|
||||||
this.__last = null;
|
this.__last = null;
|
||||||
this.__size = 0;
|
this.__size = 0;
|
||||||
this.__format = 0;
|
|
||||||
this.__style = '';
|
this.__style = '';
|
||||||
this.__indent = 0;
|
|
||||||
this.__dir = null;
|
this.__dir = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,28 +90,14 @@ export class ElementNode extends LexicalNode {
|
||||||
this.__first = prevNode.__first;
|
this.__first = prevNode.__first;
|
||||||
this.__last = prevNode.__last;
|
this.__last = prevNode.__last;
|
||||||
this.__size = prevNode.__size;
|
this.__size = prevNode.__size;
|
||||||
this.__indent = prevNode.__indent;
|
|
||||||
this.__format = prevNode.__format;
|
|
||||||
this.__style = prevNode.__style;
|
this.__style = prevNode.__style;
|
||||||
this.__dir = prevNode.__dir;
|
this.__dir = prevNode.__dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFormat(): number {
|
|
||||||
const self = this.getLatest();
|
|
||||||
return self.__format;
|
|
||||||
}
|
|
||||||
getFormatType(): ElementFormatType {
|
|
||||||
const format = this.getFormat();
|
|
||||||
return ELEMENT_FORMAT_TO_TYPE[format] || '';
|
|
||||||
}
|
|
||||||
getStyle(): string {
|
getStyle(): string {
|
||||||
const self = this.getLatest();
|
const self = this.getLatest();
|
||||||
return self.__style;
|
return self.__style;
|
||||||
}
|
}
|
||||||
getIndent(): number {
|
|
||||||
const self = this.getLatest();
|
|
||||||
return self.__indent;
|
|
||||||
}
|
|
||||||
getChildren<T extends LexicalNode>(): Array<T> {
|
getChildren<T extends LexicalNode>(): Array<T> {
|
||||||
const children: Array<T> = [];
|
const children: Array<T> = [];
|
||||||
let child: T | null = this.getFirstChild();
|
let child: T | null = this.getFirstChild();
|
||||||
|
@ -301,13 +279,6 @@ export class ElementNode extends LexicalNode {
|
||||||
const self = this.getLatest();
|
const self = this.getLatest();
|
||||||
return self.__dir;
|
return self.__dir;
|
||||||
}
|
}
|
||||||
hasFormat(type: ElementFormatType): boolean {
|
|
||||||
if (type !== '') {
|
|
||||||
const formatFlag = ELEMENT_TYPE_TO_FORMAT[type];
|
|
||||||
return (this.getFormat() & formatFlag) !== 0;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutators
|
// Mutators
|
||||||
|
|
||||||
|
@ -378,21 +349,11 @@ export class ElementNode extends LexicalNode {
|
||||||
self.__dir = direction;
|
self.__dir = direction;
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
setFormat(type: ElementFormatType): this {
|
|
||||||
const self = this.getWritable();
|
|
||||||
self.__format = type !== '' ? ELEMENT_TYPE_TO_FORMAT[type] : 0;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
setStyle(style: string): this {
|
setStyle(style: string): this {
|
||||||
const self = this.getWritable();
|
const self = this.getWritable();
|
||||||
self.__style = style || '';
|
self.__style = style || '';
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
setIndent(indentLevel: number): this {
|
|
||||||
const self = this.getWritable();
|
|
||||||
self.__indent = indentLevel;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
splice(
|
splice(
|
||||||
start: number,
|
start: number,
|
||||||
deleteCount: number,
|
deleteCount: number,
|
||||||
|
@ -528,8 +489,6 @@ export class ElementNode extends LexicalNode {
|
||||||
return {
|
return {
|
||||||
children: [],
|
children: [],
|
||||||
direction: this.getDirection(),
|
direction: this.getDirection(),
|
||||||
format: this.getFormatType(),
|
|
||||||
indent: this.getIndent(),
|
|
||||||
type: 'element',
|
type: 'element',
|
||||||
version: 1,
|
version: 1,
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,39 +19,36 @@ import type {
|
||||||
LexicalNode,
|
LexicalNode,
|
||||||
NodeKey,
|
NodeKey,
|
||||||
} from '../LexicalNode';
|
} from '../LexicalNode';
|
||||||
import type {
|
|
||||||
ElementFormatType,
|
|
||||||
SerializedElementNode,
|
|
||||||
} from './LexicalElementNode';
|
|
||||||
import type {RangeSelection} from 'lexical';
|
import type {RangeSelection} from 'lexical';
|
||||||
|
|
||||||
import {TEXT_TYPE_TO_FORMAT} from '../LexicalConstants';
|
|
||||||
import {
|
import {
|
||||||
$applyNodeReplacement,
|
$applyNodeReplacement,
|
||||||
getCachedClassNameArray,
|
getCachedClassNameArray,
|
||||||
isHTMLElement,
|
isHTMLElement,
|
||||||
} from '../LexicalUtils';
|
} from '../LexicalUtils';
|
||||||
import {ElementNode} from './LexicalElementNode';
|
import {$isTextNode} from './LexicalTextNode';
|
||||||
import {$isTextNode, TextFormatType} from './LexicalTextNode';
|
import {
|
||||||
|
commonPropertiesDifferent, deserializeCommonBlockNode,
|
||||||
|
setCommonBlockPropsFromElement,
|
||||||
|
updateElementWithCommonBlockProps
|
||||||
|
} from "./common";
|
||||||
|
import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
|
||||||
|
|
||||||
export type SerializedParagraphNode = Spread<
|
export type SerializedParagraphNode = Spread<
|
||||||
{
|
{
|
||||||
textFormat: number;
|
|
||||||
textStyle: string;
|
textStyle: string;
|
||||||
},
|
},
|
||||||
SerializedElementNode
|
SerializedCommonBlockNode
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/** @noInheritDoc */
|
/** @noInheritDoc */
|
||||||
export class ParagraphNode extends ElementNode {
|
export class ParagraphNode extends CommonBlockNode {
|
||||||
['constructor']!: KlassConstructor<typeof ParagraphNode>;
|
['constructor']!: KlassConstructor<typeof ParagraphNode>;
|
||||||
/** @internal */
|
/** @internal */
|
||||||
__textFormat: number;
|
|
||||||
__textStyle: string;
|
__textStyle: string;
|
||||||
|
|
||||||
constructor(key?: NodeKey) {
|
constructor(key?: NodeKey) {
|
||||||
super(key);
|
super(key);
|
||||||
this.__textFormat = 0;
|
|
||||||
this.__textStyle = '';
|
this.__textStyle = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,22 +56,6 @@ export class ParagraphNode extends ElementNode {
|
||||||
return 'paragraph';
|
return 'paragraph';
|
||||||
}
|
}
|
||||||
|
|
||||||
getTextFormat(): number {
|
|
||||||
const self = this.getLatest();
|
|
||||||
return self.__textFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTextFormat(type: number): this {
|
|
||||||
const self = this.getWritable();
|
|
||||||
self.__textFormat = type;
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasTextFormat(type: TextFormatType): boolean {
|
|
||||||
const formatFlag = TEXT_TYPE_TO_FORMAT[type];
|
|
||||||
return (this.getTextFormat() & formatFlag) !== 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTextStyle(): string {
|
getTextStyle(): string {
|
||||||
const self = this.getLatest();
|
const self = this.getLatest();
|
||||||
return self.__textStyle;
|
return self.__textStyle;
|
||||||
|
@ -92,8 +73,8 @@ export class ParagraphNode extends ElementNode {
|
||||||
|
|
||||||
afterCloneFrom(prevNode: this) {
|
afterCloneFrom(prevNode: this) {
|
||||||
super.afterCloneFrom(prevNode);
|
super.afterCloneFrom(prevNode);
|
||||||
this.__textFormat = prevNode.__textFormat;
|
|
||||||
this.__textStyle = prevNode.__textStyle;
|
this.__textStyle = prevNode.__textStyle;
|
||||||
|
copyCommonBlockProperties(prevNode, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// View
|
// View
|
||||||
|
@ -105,6 +86,9 @@ export class ParagraphNode extends ElementNode {
|
||||||
const domClassList = dom.classList;
|
const domClassList = dom.classList;
|
||||||
domClassList.add(...classNames);
|
domClassList.add(...classNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateElementWithCommonBlockProps(dom, this);
|
||||||
|
|
||||||
return dom;
|
return dom;
|
||||||
}
|
}
|
||||||
updateDOM(
|
updateDOM(
|
||||||
|
@ -112,7 +96,7 @@ export class ParagraphNode extends ElementNode {
|
||||||
dom: HTMLElement,
|
dom: HTMLElement,
|
||||||
config: EditorConfig,
|
config: EditorConfig,
|
||||||
): boolean {
|
): boolean {
|
||||||
return false;
|
return commonPropertiesDifferent(prevNode, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap | null {
|
static importDOM(): DOMConversionMap | null {
|
||||||
|
@ -131,16 +115,6 @@ export class ParagraphNode extends ElementNode {
|
||||||
if (this.isEmpty()) {
|
if (this.isEmpty()) {
|
||||||
element.append(document.createElement('br'));
|
element.append(document.createElement('br'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatType = this.getFormatType();
|
|
||||||
element.style.textAlign = formatType;
|
|
||||||
|
|
||||||
const indent = this.getIndent();
|
|
||||||
if (indent > 0) {
|
|
||||||
// padding-inline-start is not widely supported in email HTML, but
|
|
||||||
// Lexical Reconciler uses padding-inline-start. Using text-indent instead.
|
|
||||||
element.style.textIndent = `${indent * 20}px`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -150,16 +124,13 @@ export class ParagraphNode extends ElementNode {
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode {
|
static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode {
|
||||||
const node = $createParagraphNode();
|
const node = $createParagraphNode();
|
||||||
node.setFormat(serializedNode.format);
|
deserializeCommonBlockNode(serializedNode, node);
|
||||||
node.setIndent(serializedNode.indent);
|
|
||||||
node.setTextFormat(serializedNode.textFormat);
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
exportJSON(): SerializedParagraphNode {
|
exportJSON(): SerializedParagraphNode {
|
||||||
return {
|
return {
|
||||||
...super.exportJSON(),
|
...super.exportJSON(),
|
||||||
textFormat: this.getTextFormat(),
|
|
||||||
textStyle: this.getTextStyle(),
|
textStyle: this.getTextStyle(),
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
version: 1,
|
version: 1,
|
||||||
|
@ -173,11 +144,9 @@ export class ParagraphNode extends ElementNode {
|
||||||
restoreSelection: boolean,
|
restoreSelection: boolean,
|
||||||
): ParagraphNode {
|
): ParagraphNode {
|
||||||
const newElement = $createParagraphNode();
|
const newElement = $createParagraphNode();
|
||||||
newElement.setTextFormat(rangeSelection.format);
|
|
||||||
newElement.setTextStyle(rangeSelection.style);
|
newElement.setTextStyle(rangeSelection.style);
|
||||||
const direction = this.getDirection();
|
const direction = this.getDirection();
|
||||||
newElement.setDirection(direction);
|
newElement.setDirection(direction);
|
||||||
newElement.setFormat(this.getFormatType());
|
|
||||||
newElement.setStyle(this.getTextStyle());
|
newElement.setStyle(this.getTextStyle());
|
||||||
this.insertAfter(newElement, restoreSelection);
|
this.insertAfter(newElement, restoreSelection);
|
||||||
return newElement;
|
return newElement;
|
||||||
|
@ -210,13 +179,7 @@ export class ParagraphNode extends ElementNode {
|
||||||
|
|
||||||
function $convertParagraphElement(element: HTMLElement): DOMConversionOutput {
|
function $convertParagraphElement(element: HTMLElement): DOMConversionOutput {
|
||||||
const node = $createParagraphNode();
|
const node = $createParagraphNode();
|
||||||
if (element.style) {
|
setCommonBlockPropsFromElement(element, node);
|
||||||
node.setFormat(element.style.textAlign as ElementFormatType);
|
|
||||||
const indent = parseInt(element.style.textIndent, 10) / 20;
|
|
||||||
if (indent > 0) {
|
|
||||||
node.setIndent(indent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {node};
|
return {node};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,8 +99,6 @@ export class RootNode extends ElementNode {
|
||||||
static importJSON(serializedNode: SerializedRootNode): RootNode {
|
static importJSON(serializedNode: SerializedRootNode): RootNode {
|
||||||
// We don't create a root, and instead use the existing root.
|
// We don't create a root, and instead use the existing root.
|
||||||
const node = $getRoot();
|
const node = $getRoot();
|
||||||
node.setFormat(serializedNode.format);
|
|
||||||
node.setIndent(serializedNode.indent);
|
|
||||||
node.setDirection(serializedNode.direction);
|
node.setDirection(serializedNode.direction);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
@ -109,8 +107,6 @@ export class RootNode extends ElementNode {
|
||||||
return {
|
return {
|
||||||
children: [],
|
children: [],
|
||||||
direction: this.getDirection(),
|
direction: this.getDirection(),
|
||||||
format: this.getFormatType(),
|
|
||||||
indent: this.getIndent(),
|
|
||||||
type: 'root',
|
type: 'root',
|
||||||
version: 1,
|
version: 1,
|
||||||
};
|
};
|
||||||
|
|
|
@ -84,8 +84,6 @@ describe('LexicalElementNode tests', () => {
|
||||||
expect(node.exportJSON()).toStrictEqual({
|
expect(node.exportJSON()).toStrictEqual({
|
||||||
children: [],
|
children: [],
|
||||||
direction: null,
|
direction: null,
|
||||||
format: '',
|
|
||||||
indent: 0,
|
|
||||||
type: 'test_block',
|
type: 'test_block',
|
||||||
version: 1,
|
version: 1,
|
||||||
});
|
});
|
||||||
|
|
|
@ -48,11 +48,11 @@ describe('LexicalParagraphNode tests', () => {
|
||||||
// logic is in place in the corresponding importJSON method
|
// logic is in place in the corresponding importJSON method
|
||||||
// to accomodate these changes.
|
// to accomodate these changes.
|
||||||
expect(node.exportJSON()).toStrictEqual({
|
expect(node.exportJSON()).toStrictEqual({
|
||||||
|
alignment: '',
|
||||||
children: [],
|
children: [],
|
||||||
direction: null,
|
direction: null,
|
||||||
format: '',
|
id: '',
|
||||||
indent: 0,
|
inset: 0,
|
||||||
textFormat: 0,
|
|
||||||
textStyle: '',
|
textStyle: '',
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
version: 1,
|
version: 1,
|
||||||
|
@ -127,6 +127,21 @@ describe('LexicalParagraphNode tests', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('id is supported', async () => {
|
||||||
|
const {editor} = testEnv;
|
||||||
|
let paragraphNode: ParagraphNode;
|
||||||
|
|
||||||
|
await editor.update(() => {
|
||||||
|
paragraphNode = new ParagraphNode();
|
||||||
|
paragraphNode.setId('testid')
|
||||||
|
$getRoot().append(paragraphNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(testEnv.innerHTML).toBe(
|
||||||
|
'<p id="testid"><br></p>',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('$createParagraphNode()', async () => {
|
test('$createParagraphNode()', async () => {
|
||||||
const {editor} = testEnv;
|
const {editor} = testEnv;
|
||||||
|
|
||||||
|
|
|
@ -77,8 +77,6 @@ describe('LexicalRootNode tests', () => {
|
||||||
expect(node.exportJSON()).toStrictEqual({
|
expect(node.exportJSON()).toStrictEqual({
|
||||||
children: [],
|
children: [],
|
||||||
direction: null,
|
direction: null,
|
||||||
format: '',
|
|
||||||
indent: 0,
|
|
||||||
type: 'root',
|
type: 'root',
|
||||||
version: 1,
|
version: 1,
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,21 +10,14 @@ import {
|
||||||
$insertDataTransferForPlainText,
|
$insertDataTransferForPlainText,
|
||||||
$insertDataTransferForRichText,
|
$insertDataTransferForRichText,
|
||||||
} from '@lexical/clipboard';
|
} from '@lexical/clipboard';
|
||||||
import {$createListItemNode, $createListNode} from '@lexical/list';
|
|
||||||
import {$createHeadingNode, registerRichText} from '@lexical/rich-text';
|
|
||||||
import {
|
import {
|
||||||
$createParagraphNode,
|
$createParagraphNode,
|
||||||
$createRangeSelection,
|
|
||||||
$createTabNode,
|
$createTabNode,
|
||||||
$createTextNode,
|
|
||||||
$getRoot,
|
$getRoot,
|
||||||
$getSelection,
|
$getSelection,
|
||||||
$insertNodes,
|
$insertNodes,
|
||||||
$isElementNode,
|
|
||||||
$isRangeSelection,
|
$isRangeSelection,
|
||||||
$isTextNode,
|
|
||||||
$setSelection,
|
|
||||||
KEY_TAB_COMMAND,
|
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -41,9 +41,7 @@ import {
|
||||||
$setCompositionKey,
|
$setCompositionKey,
|
||||||
getEditorStateTextContent,
|
getEditorStateTextContent,
|
||||||
} from '../../../LexicalUtils';
|
} from '../../../LexicalUtils';
|
||||||
import {Text} from "@codemirror/state";
|
|
||||||
import {$generateHtmlFromNodes} from "@lexical/html";
|
import {$generateHtmlFromNodes} from "@lexical/html";
|
||||||
import {formatBold} from "@lexical/selection/__tests__/utils";
|
|
||||||
|
|
||||||
const editorConfig = Object.freeze({
|
const editorConfig = Object.freeze({
|
||||||
namespace: '',
|
namespace: '',
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
import {LexicalNode, Spread} from "lexical";
|
import {sizeToPixels} from "../../../utils/dom";
|
||||||
import type {SerializedElementNode} from "lexical/nodes/LexicalElementNode";
|
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
|
||||||
import {el, sizeToPixels} from "../utils/dom";
|
|
||||||
|
|
||||||
export type CommonBlockAlignment = 'left' | 'right' | 'center' | 'justify' | '';
|
export type CommonBlockAlignment = 'left' | 'right' | 'center' | 'justify' | '';
|
||||||
const validAlignments: CommonBlockAlignment[] = ['left', 'right', 'center', 'justify'];
|
const validAlignments: CommonBlockAlignment[] = ['left', 'right', 'center', 'justify'];
|
||||||
|
|
||||||
type EditorNodeDirection = 'ltr' | 'rtl' | null;
|
type EditorNodeDirection = 'ltr' | 'rtl' | null;
|
||||||
|
|
||||||
export type SerializedCommonBlockNode = Spread<{
|
|
||||||
id: string;
|
|
||||||
alignment: CommonBlockAlignment;
|
|
||||||
inset: number;
|
|
||||||
}, SerializedElementNode>
|
|
||||||
|
|
||||||
export interface NodeHasAlignment {
|
export interface NodeHasAlignment {
|
||||||
readonly __alignment: CommonBlockAlignment;
|
readonly __alignment: CommonBlockAlignment;
|
||||||
setAlignment(alignment: CommonBlockAlignment): void;
|
setAlignment(alignment: CommonBlockAlignment): void;
|
||||||
|
@ -37,7 +30,7 @@ export interface NodeHasDirection {
|
||||||
getDirection(): EditorNodeDirection;
|
getDirection(): EditorNodeDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommonBlockInterface extends NodeHasId, NodeHasAlignment, NodeHasInset, NodeHasDirection {}
|
export interface CommonBlockInterface extends NodeHasId, NodeHasAlignment, NodeHasInset, NodeHasDirection {}
|
||||||
|
|
||||||
export function extractAlignmentFromElement(element: HTMLElement): CommonBlockAlignment {
|
export function extractAlignmentFromElement(element: HTMLElement): CommonBlockAlignment {
|
||||||
const textAlignStyle: string = element.style.textAlign || '';
|
const textAlignStyle: string = element.style.textAlign || '';
|
|
@ -206,7 +206,7 @@ describe('LexicalHeadlessEditor', () => {
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
expect(html).toBe(
|
expect(html).toBe(
|
||||||
'<p>hello world</p>',
|
'<p dir="ltr">hello world</p>',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,13 +13,14 @@ import {createHeadlessEditor} from '@lexical/headless';
|
||||||
import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
|
import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
|
||||||
import {LinkNode} from '@lexical/link';
|
import {LinkNode} from '@lexical/link';
|
||||||
import {ListItemNode, ListNode} from '@lexical/list';
|
import {ListItemNode, ListNode} from '@lexical/list';
|
||||||
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
|
|
||||||
import {
|
import {
|
||||||
$createParagraphNode,
|
$createParagraphNode,
|
||||||
$createRangeSelection,
|
$createRangeSelection,
|
||||||
$createTextNode,
|
$createTextNode,
|
||||||
$getRoot,
|
$getRoot,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
|
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
|
||||||
|
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
|
||||||
|
|
||||||
describe('HTML', () => {
|
describe('HTML', () => {
|
||||||
type Input = Array<{
|
type Input = Array<{
|
||||||
|
@ -175,7 +176,7 @@ describe('HTML', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(html).toBe(
|
expect(html).toBe(
|
||||||
'<p style="text-align: center;">Hello world!</p>',
|
'<p class="align-center">Hello world!</p>',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -205,7 +206,7 @@ describe('HTML', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(html).toBe(
|
expect(html).toBe(
|
||||||
'<p style="text-align: center;">Hello world!</p>',
|
'<p class="align-center">Hello world!</p>',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -327,9 +327,6 @@ function wrapContinuousInlines(
|
||||||
for (let i = 0; i < nodes.length; i++) {
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
const node = nodes[i];
|
const node = nodes[i];
|
||||||
if ($isBlockElementNode(node)) {
|
if ($isBlockElementNode(node)) {
|
||||||
if (textAlign && !node.getFormat()) {
|
|
||||||
node.setFormat(textAlign);
|
|
||||||
}
|
|
||||||
out.push(node);
|
out.push(node);
|
||||||
} else {
|
} else {
|
||||||
continuousInlines.push(node);
|
continuousInlines.push(node);
|
||||||
|
@ -338,7 +335,6 @@ function wrapContinuousInlines(
|
||||||
(i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1]))
|
(i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1]))
|
||||||
) {
|
) {
|
||||||
const wrapper = createWrapperFn();
|
const wrapper = createWrapperFn();
|
||||||
wrapper.setFormat(textAlign);
|
|
||||||
wrapper.append(...continuousInlines);
|
wrapper.append(...continuousInlines);
|
||||||
out.push(wrapper);
|
out.push(wrapper);
|
||||||
continuousInlines = [];
|
continuousInlines = [];
|
||||||
|
|
|
@ -162,8 +162,6 @@ export class LinkNode extends ElementNode {
|
||||||
target: serializedNode.target,
|
target: serializedNode.target,
|
||||||
title: serializedNode.title,
|
title: serializedNode.title,
|
||||||
});
|
});
|
||||||
node.setFormat(serializedNode.format);
|
|
||||||
node.setIndent(serializedNode.indent);
|
|
||||||
node.setDirection(serializedNode.direction);
|
node.setDirection(serializedNode.direction);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
@ -402,8 +400,6 @@ export class AutoLinkNode extends LinkNode {
|
||||||
target: serializedNode.target,
|
target: serializedNode.target,
|
||||||
title: serializedNode.title,
|
title: serializedNode.title,
|
||||||
});
|
});
|
||||||
node.setFormat(serializedNode.format);
|
|
||||||
node.setIndent(serializedNode.indent);
|
|
||||||
node.setDirection(serializedNode.direction);
|
node.setDirection(serializedNode.direction);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ import type {
|
||||||
DOMConversionOutput,
|
DOMConversionOutput,
|
||||||
DOMExportOutput,
|
DOMExportOutput,
|
||||||
EditorConfig,
|
EditorConfig,
|
||||||
EditorThemeClasses,
|
|
||||||
LexicalNode,
|
LexicalNode,
|
||||||
NodeKey,
|
NodeKey,
|
||||||
ParagraphNode,
|
ParagraphNode,
|
||||||
|
@ -22,10 +21,6 @@ import type {
|
||||||
Spread,
|
Spread,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
|
|
||||||
import {
|
|
||||||
addClassNamesToElement,
|
|
||||||
removeClassNamesFromElement,
|
|
||||||
} from '@lexical/utils';
|
|
||||||
import {
|
import {
|
||||||
$applyNodeReplacement,
|
$applyNodeReplacement,
|
||||||
$createParagraphNode,
|
$createParagraphNode,
|
||||||
|
@ -36,11 +31,11 @@ import {
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
import invariant from 'lexical/shared/invariant';
|
import invariant from 'lexical/shared/invariant';
|
||||||
import normalizeClassNames from 'lexical/shared/normalizeClassNames';
|
|
||||||
|
|
||||||
import {$createListNode, $isListNode} from './';
|
import {$createListNode, $isListNode} from './';
|
||||||
import {$handleIndent, $handleOutdent, mergeLists} from './formatList';
|
import {mergeLists} from './formatList';
|
||||||
import {isNestedListNode} from './utils';
|
import {isNestedListNode} from './utils';
|
||||||
|
import {el} from "../../utils/dom";
|
||||||
|
|
||||||
export type SerializedListItemNode = Spread<
|
export type SerializedListItemNode = Spread<
|
||||||
{
|
{
|
||||||
|
@ -74,11 +69,17 @@ export class ListItemNode extends ElementNode {
|
||||||
createDOM(config: EditorConfig): HTMLElement {
|
createDOM(config: EditorConfig): HTMLElement {
|
||||||
const element = document.createElement('li');
|
const element = document.createElement('li');
|
||||||
const parent = this.getParent();
|
const parent = this.getParent();
|
||||||
|
|
||||||
if ($isListNode(parent) && parent.getListType() === 'check') {
|
if ($isListNode(parent) && parent.getListType() === 'check') {
|
||||||
updateListItemChecked(element, this, null, parent);
|
updateListItemChecked(element, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
element.value = this.__value;
|
element.value = this.__value;
|
||||||
$setListItemThemeClassNames(element, config.theme, this);
|
|
||||||
|
if ($hasNestedListWithoutLabel(this)) {
|
||||||
|
element.style.listStyle = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,11 +90,12 @@ export class ListItemNode extends ElementNode {
|
||||||
): boolean {
|
): boolean {
|
||||||
const parent = this.getParent();
|
const parent = this.getParent();
|
||||||
if ($isListNode(parent) && parent.getListType() === 'check') {
|
if ($isListNode(parent) && parent.getListType() === 'check') {
|
||||||
updateListItemChecked(dom, this, prevNode, parent);
|
updateListItemChecked(dom, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dom.style.listStyle = $hasNestedListWithoutLabel(this) ? 'none' : '';
|
||||||
// @ts-expect-error - this is always HTMLListItemElement
|
// @ts-expect-error - this is always HTMLListItemElement
|
||||||
dom.value = this.__value;
|
dom.value = this.__value;
|
||||||
$setListItemThemeClassNames(dom, config.theme, this);
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -126,14 +128,26 @@ export class ListItemNode extends ElementNode {
|
||||||
const node = $createListItemNode();
|
const node = $createListItemNode();
|
||||||
node.setChecked(serializedNode.checked);
|
node.setChecked(serializedNode.checked);
|
||||||
node.setValue(serializedNode.value);
|
node.setValue(serializedNode.value);
|
||||||
node.setFormat(serializedNode.format);
|
|
||||||
node.setDirection(serializedNode.direction);
|
node.setDirection(serializedNode.direction);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
||||||
const element = this.createDOM(editor._config);
|
const element = this.createDOM(editor._config);
|
||||||
element.style.textAlign = this.getFormatType();
|
|
||||||
|
if (element.classList.contains('task-list-item')) {
|
||||||
|
const input = el('input', {
|
||||||
|
type: 'checkbox',
|
||||||
|
disabled: 'disabled',
|
||||||
|
});
|
||||||
|
if (element.hasAttribute('checked')) {
|
||||||
|
input.setAttribute('checked', 'checked');
|
||||||
|
element.removeAttribute('checked');
|
||||||
|
}
|
||||||
|
|
||||||
|
element.prepend(input);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
element,
|
element,
|
||||||
};
|
};
|
||||||
|
@ -172,7 +186,6 @@ export class ListItemNode extends ElementNode {
|
||||||
if ($isListItemNode(replaceWithNode)) {
|
if ($isListItemNode(replaceWithNode)) {
|
||||||
return super.replace(replaceWithNode);
|
return super.replace(replaceWithNode);
|
||||||
}
|
}
|
||||||
this.setIndent(0);
|
|
||||||
const list = this.getParentOrThrow();
|
const list = this.getParentOrThrow();
|
||||||
if (!$isListNode(list)) {
|
if (!$isListNode(list)) {
|
||||||
return replaceWithNode;
|
return replaceWithNode;
|
||||||
|
@ -351,41 +364,6 @@ export class ListItemNode extends ElementNode {
|
||||||
this.setChecked(!this.__checked);
|
this.setChecked(!this.__checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
getIndent(): number {
|
|
||||||
// If we don't have a parent, we are likely serializing
|
|
||||||
const parent = this.getParent();
|
|
||||||
if (parent === null) {
|
|
||||||
return this.getLatest().__indent;
|
|
||||||
}
|
|
||||||
// ListItemNode should always have a ListNode for a parent.
|
|
||||||
let listNodeParent = parent.getParentOrThrow();
|
|
||||||
let indentLevel = 0;
|
|
||||||
while ($isListItemNode(listNodeParent)) {
|
|
||||||
listNodeParent = listNodeParent.getParentOrThrow().getParentOrThrow();
|
|
||||||
indentLevel++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return indentLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIndent(indent: number): this {
|
|
||||||
invariant(typeof indent === 'number', 'Invalid indent value.');
|
|
||||||
indent = Math.floor(indent);
|
|
||||||
invariant(indent >= 0, 'Indent value must be non-negative.');
|
|
||||||
let currentIndent = this.getIndent();
|
|
||||||
while (currentIndent !== indent) {
|
|
||||||
if (currentIndent < indent) {
|
|
||||||
$handleIndent(this);
|
|
||||||
currentIndent++;
|
|
||||||
} else {
|
|
||||||
$handleOutdent(this);
|
|
||||||
currentIndent--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated @internal */
|
/** @deprecated @internal */
|
||||||
canInsertAfter(node: LexicalNode): boolean {
|
canInsertAfter(node: LexicalNode): boolean {
|
||||||
return $isListItemNode(node);
|
return $isListItemNode(node);
|
||||||
|
@ -428,89 +406,33 @@ export class ListItemNode extends ElementNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function $setListItemThemeClassNames(
|
function $hasNestedListWithoutLabel(node: ListItemNode): boolean {
|
||||||
dom: HTMLElement,
|
const children = node.getChildren();
|
||||||
editorThemeClasses: EditorThemeClasses,
|
let hasLabel = false;
|
||||||
node: ListItemNode,
|
let hasNestedList = false;
|
||||||
): void {
|
|
||||||
const classesToAdd = [];
|
|
||||||
const classesToRemove = [];
|
|
||||||
const listTheme = editorThemeClasses.list;
|
|
||||||
const listItemClassName = listTheme ? listTheme.listitem : undefined;
|
|
||||||
let nestedListItemClassName;
|
|
||||||
|
|
||||||
if (listTheme && listTheme.nested) {
|
for (const child of children) {
|
||||||
nestedListItemClassName = listTheme.nested.listitem;
|
if ($isListNode(child)) {
|
||||||
}
|
hasNestedList = true;
|
||||||
|
} else if (child.getTextContent().trim().length > 0) {
|
||||||
if (listItemClassName !== undefined) {
|
hasLabel = true;
|
||||||
classesToAdd.push(...normalizeClassNames(listItemClassName));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listTheme) {
|
|
||||||
const parentNode = node.getParent();
|
|
||||||
const isCheckList =
|
|
||||||
$isListNode(parentNode) && parentNode.getListType() === 'check';
|
|
||||||
const checked = node.getChecked();
|
|
||||||
|
|
||||||
if (!isCheckList || checked) {
|
|
||||||
classesToRemove.push(listTheme.listitemUnchecked);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isCheckList || !checked) {
|
|
||||||
classesToRemove.push(listTheme.listitemChecked);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCheckList) {
|
|
||||||
classesToAdd.push(
|
|
||||||
checked ? listTheme.listitemChecked : listTheme.listitemUnchecked,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nestedListItemClassName !== undefined) {
|
return hasNestedList && !hasLabel;
|
||||||
const nestedListItemClasses = normalizeClassNames(nestedListItemClassName);
|
|
||||||
|
|
||||||
if (node.getChildren().some((child) => $isListNode(child))) {
|
|
||||||
classesToAdd.push(...nestedListItemClasses);
|
|
||||||
} else {
|
|
||||||
classesToRemove.push(...nestedListItemClasses);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (classesToRemove.length > 0) {
|
|
||||||
removeClassNamesFromElement(dom, ...classesToRemove);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (classesToAdd.length > 0) {
|
|
||||||
addClassNamesToElement(dom, ...classesToAdd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateListItemChecked(
|
function updateListItemChecked(
|
||||||
dom: HTMLElement,
|
dom: HTMLElement,
|
||||||
listItemNode: ListItemNode,
|
listItemNode: ListItemNode,
|
||||||
prevListItemNode: ListItemNode | null,
|
|
||||||
listNode: ListNode,
|
|
||||||
): void {
|
): void {
|
||||||
// Only add attributes for leaf list items
|
// Only set task list attrs for leaf list items
|
||||||
if ($isListNode(listItemNode.getFirstChild())) {
|
const shouldBeTaskItem = !$isListNode(listItemNode.getFirstChild());
|
||||||
dom.removeAttribute('role');
|
dom.classList.toggle('task-list-item', shouldBeTaskItem);
|
||||||
dom.removeAttribute('tabIndex');
|
if (listItemNode.__checked) {
|
||||||
dom.removeAttribute('aria-checked');
|
dom.setAttribute('checked', 'checked');
|
||||||
} else {
|
} else {
|
||||||
dom.setAttribute('role', 'checkbox');
|
dom.removeAttribute('checked');
|
||||||
dom.setAttribute('tabIndex', '-1');
|
|
||||||
|
|
||||||
if (
|
|
||||||
!prevListItemNode ||
|
|
||||||
listItemNode.__checked !== prevListItemNode.__checked
|
|
||||||
) {
|
|
||||||
dom.setAttribute(
|
|
||||||
'aria-checked',
|
|
||||||
listItemNode.getChecked() ? 'true' : 'false',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,9 +36,11 @@ import {
|
||||||
updateChildrenListItemValue,
|
updateChildrenListItemValue,
|
||||||
} from './formatList';
|
} from './formatList';
|
||||||
import {$getListDepth, $wrapInListItem} from './utils';
|
import {$getListDepth, $wrapInListItem} from './utils';
|
||||||
|
import {extractDirectionFromElement} from "lexical/nodes/common";
|
||||||
|
|
||||||
export type SerializedListNode = Spread<
|
export type SerializedListNode = Spread<
|
||||||
{
|
{
|
||||||
|
id: string;
|
||||||
listType: ListType;
|
listType: ListType;
|
||||||
start: number;
|
start: number;
|
||||||
tag: ListNodeTagType;
|
tag: ListNodeTagType;
|
||||||
|
@ -58,15 +60,18 @@ export class ListNode extends ElementNode {
|
||||||
__start: number;
|
__start: number;
|
||||||
/** @internal */
|
/** @internal */
|
||||||
__listType: ListType;
|
__listType: ListType;
|
||||||
|
/** @internal */
|
||||||
|
__id: string = '';
|
||||||
|
|
||||||
static getType(): string {
|
static getType(): string {
|
||||||
return 'list';
|
return 'list';
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: ListNode): ListNode {
|
static clone(node: ListNode): ListNode {
|
||||||
const listType = node.__listType || TAG_TO_LIST_TYPE[node.__tag];
|
const newNode = new ListNode(node.__listType, node.__start, node.__key);
|
||||||
|
newNode.__id = node.__id;
|
||||||
return new ListNode(listType, node.__start, node.__key);
|
newNode.__dir = node.__dir;
|
||||||
|
return newNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(listType: ListType, start: number, key?: NodeKey) {
|
constructor(listType: ListType, start: number, key?: NodeKey) {
|
||||||
|
@ -81,6 +86,16 @@ export class ListNode extends ElementNode {
|
||||||
return this.__tag;
|
return this.__tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setId(id: string) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): string {
|
||||||
|
const self = this.getLatest();
|
||||||
|
return self.__id;
|
||||||
|
}
|
||||||
|
|
||||||
setListType(type: ListType): void {
|
setListType(type: ListType): void {
|
||||||
const writable = this.getWritable();
|
const writable = this.getWritable();
|
||||||
writable.__listType = type;
|
writable.__listType = type;
|
||||||
|
@ -108,6 +123,14 @@ export class ListNode extends ElementNode {
|
||||||
dom.__lexicalListType = this.__listType;
|
dom.__lexicalListType = this.__listType;
|
||||||
$setListThemeClassNames(dom, config.theme, this);
|
$setListThemeClassNames(dom, config.theme, this);
|
||||||
|
|
||||||
|
if (this.__id) {
|
||||||
|
dom.setAttribute('id', this.__id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.__dir) {
|
||||||
|
dom.setAttribute('dir', this.__dir);
|
||||||
|
}
|
||||||
|
|
||||||
return dom;
|
return dom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +139,11 @@ export class ListNode extends ElementNode {
|
||||||
dom: HTMLElement,
|
dom: HTMLElement,
|
||||||
config: EditorConfig,
|
config: EditorConfig,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (prevNode.__tag !== this.__tag) {
|
if (
|
||||||
|
prevNode.__tag !== this.__tag
|
||||||
|
|| prevNode.__dir !== this.__dir
|
||||||
|
|| prevNode.__id !== this.__id
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,8 +175,7 @@ export class ListNode extends ElementNode {
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedListNode): ListNode {
|
static importJSON(serializedNode: SerializedListNode): ListNode {
|
||||||
const node = $createListNode(serializedNode.listType, serializedNode.start);
|
const node = $createListNode(serializedNode.listType, serializedNode.start);
|
||||||
node.setFormat(serializedNode.format);
|
node.setId(serializedNode.id);
|
||||||
node.setIndent(serializedNode.indent);
|
|
||||||
node.setDirection(serializedNode.direction);
|
node.setDirection(serializedNode.direction);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
@ -177,6 +203,7 @@ export class ListNode extends ElementNode {
|
||||||
tag: this.getTag(),
|
tag: this.getTag(),
|
||||||
type: 'list',
|
type: 'list',
|
||||||
version: 1,
|
version: 1,
|
||||||
|
id: this.__id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,28 +304,21 @@ function $setListThemeClassNames(
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This function normalizes the children of a ListNode after the conversion from HTML,
|
* This function is a custom normalization function to allow nested lists within list item elements.
|
||||||
* ensuring that they are all ListItemNodes and contain either a single nested ListNode
|
* Original taken from https://github.com/facebook/lexical/blob/6e10210fd1e113ccfafdc999b1d896733c5c5bea/packages/lexical-list/src/LexicalListNode.ts#L284-L303
|
||||||
* or some other inline content.
|
* With modifications made.
|
||||||
*/
|
*/
|
||||||
function $normalizeChildren(nodes: Array<LexicalNode>): Array<ListItemNode> {
|
function $normalizeChildren(nodes: Array<LexicalNode>): Array<ListItemNode> {
|
||||||
const normalizedListItems: Array<ListItemNode> = [];
|
const normalizedListItems: Array<ListItemNode> = [];
|
||||||
for (let i = 0; i < nodes.length; i++) {
|
|
||||||
const node = nodes[i];
|
for (const node of nodes) {
|
||||||
if ($isListItemNode(node)) {
|
if ($isListItemNode(node)) {
|
||||||
normalizedListItems.push(node);
|
normalizedListItems.push(node);
|
||||||
const children = node.getChildren();
|
|
||||||
if (children.length > 1) {
|
|
||||||
children.forEach((child) => {
|
|
||||||
if ($isListNode(child)) {
|
|
||||||
normalizedListItems.push($wrapInListItem(child));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
normalizedListItems.push($wrapInListItem(node));
|
normalizedListItems.push($wrapInListItem(node));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalizedListItems;
|
return normalizedListItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,6 +354,14 @@ function $convertListNode(domNode: HTMLElement): DOMConversionOutput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (domNode.id && node) {
|
||||||
|
node.setId(domNode.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domNode.dir && node) {
|
||||||
|
node.setDirection(extractDirectionFromElement(domNode));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
after: $normalizeChildren,
|
after: $normalizeChildren,
|
||||||
node,
|
node,
|
||||||
|
|
|
@ -62,7 +62,7 @@ describe('LexicalListItemNode tests', () => {
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
listItemNode.createDOM(editorConfig).outerHTML,
|
listItemNode.createDOM(editorConfig).outerHTML,
|
||||||
html`
|
html`
|
||||||
<li value="1" class="my-listItem-item-class"></li>
|
<li value="1"></li>
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ describe('LexicalListItemNode tests', () => {
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
domElement.outerHTML,
|
domElement.outerHTML,
|
||||||
html`
|
html`
|
||||||
<li value="1" class="my-listItem-item-class"></li>
|
<li value="1"></li>
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
const newListItemNode = new ListItemNode();
|
const newListItemNode = new ListItemNode();
|
||||||
|
@ -106,7 +106,7 @@ describe('LexicalListItemNode tests', () => {
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
domElement.outerHTML,
|
domElement.outerHTML,
|
||||||
html`
|
html`
|
||||||
<li value="1" class="my-listItem-item-class"></li>
|
<li value="1"></li>
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -125,7 +125,7 @@ describe('LexicalListItemNode tests', () => {
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
domElement.outerHTML,
|
domElement.outerHTML,
|
||||||
html`
|
html`
|
||||||
<li value="1" class="my-listItem-item-class"></li>
|
<li value="1"></li>
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
const nestedListNode = new ListNode('bullet', 1);
|
const nestedListNode = new ListNode('bullet', 1);
|
||||||
|
@ -142,7 +142,7 @@ describe('LexicalListItemNode tests', () => {
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
domElement.outerHTML,
|
domElement.outerHTML,
|
||||||
html`
|
html`
|
||||||
<li value="1" class="my-listItem-item-class my-nested-list-listItem-class"></li>
|
<li value="1" style="list-style: none;"></li>
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -486,53 +486,43 @@ describe('LexicalListItemNode tests', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
testEnv.outerHTML,
|
testEnv.innerHTML,
|
||||||
html`
|
html`
|
||||||
<div
|
<ul>
|
||||||
contenteditable="true"
|
<li value="1" style="list-style: none;">
|
||||||
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
|
<ul>
|
||||||
data-lexical-editor="true">
|
<li value="1">
|
||||||
<ul>
|
<span data-lexical-text="true">A</span>
|
||||||
<li value="1">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">A</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">x</span>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
<li value="2">
|
||||||
<li value="1">
|
<span data-lexical-text="true">B</span>
|
||||||
<span data-lexical-text="true">x</span>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
<li value="2">
|
|
||||||
<span data-lexical-text="true">B</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await editor.update(() => x.remove());
|
await editor.update(() => x.remove());
|
||||||
|
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
testEnv.outerHTML,
|
testEnv.innerHTML,
|
||||||
html`
|
html`
|
||||||
<div
|
<ul>
|
||||||
contenteditable="true"
|
<li value="1" style="list-style: none;">
|
||||||
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
|
<ul>
|
||||||
data-lexical-editor="true">
|
<li value="1">
|
||||||
<ul>
|
<span data-lexical-text="true">A</span>
|
||||||
<li value="1">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">A</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">B</span>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
<li value="1">
|
|
||||||
<span data-lexical-text="true">B</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -566,53 +556,43 @@ describe('LexicalListItemNode tests', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
testEnv.outerHTML,
|
testEnv.innerHTML,
|
||||||
html`
|
html`
|
||||||
<div
|
<ul>
|
||||||
contenteditable="true"
|
<li value="1">
|
||||||
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
|
<span data-lexical-text="true">A</span>
|
||||||
data-lexical-editor="true">
|
</li>
|
||||||
<ul>
|
<li value="2">
|
||||||
<li value="1">
|
<span data-lexical-text="true">x</span>
|
||||||
<span data-lexical-text="true">A</span>
|
</li>
|
||||||
</li>
|
<li value="3" style="list-style: none;">
|
||||||
<li value="2">
|
<ul>
|
||||||
<span data-lexical-text="true">x</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">B</span>
|
||||||
<li value="3">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">B</span>
|
</ul>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await editor.update(() => x.remove());
|
await editor.update(() => x.remove());
|
||||||
|
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
testEnv.outerHTML,
|
testEnv.innerHTML,
|
||||||
html`
|
html`
|
||||||
<div
|
<ul>
|
||||||
contenteditable="true"
|
<li value="1">
|
||||||
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
|
<span data-lexical-text="true">A</span>
|
||||||
data-lexical-editor="true">
|
</li>
|
||||||
<ul>
|
<li value="2" style="list-style: none;">
|
||||||
<li value="1">
|
<ul>
|
||||||
<span data-lexical-text="true">A</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">B</span>
|
||||||
<li value="2">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">B</span>
|
</ul>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -650,57 +630,47 @@ describe('LexicalListItemNode tests', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
testEnv.outerHTML,
|
testEnv.innerHTML,
|
||||||
html`
|
html`
|
||||||
<div
|
<ul>
|
||||||
contenteditable="true"
|
<li value="1" style="list-style: none;">
|
||||||
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
|
<ul>
|
||||||
data-lexical-editor="true">
|
<li value="1">
|
||||||
<ul>
|
<span data-lexical-text="true">A</span>
|
||||||
<li value="1">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">A</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">x</span>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
<li value="2" style="list-style: none;">
|
||||||
<li value="1">
|
<ul>
|
||||||
<span data-lexical-text="true">x</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">B</span>
|
||||||
<li value="2">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">B</span>
|
</ul>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await editor.update(() => x.remove());
|
await editor.update(() => x.remove());
|
||||||
|
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
testEnv.outerHTML,
|
testEnv.innerHTML,
|
||||||
html`
|
html`
|
||||||
<div
|
<ul>
|
||||||
contenteditable="true"
|
<li value="1" style="list-style: none;">
|
||||||
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
|
<ul>
|
||||||
data-lexical-editor="true">
|
<li value="1">
|
||||||
<ul>
|
<span data-lexical-text="true">A</span>
|
||||||
<li value="1">
|
</li>
|
||||||
<ul>
|
<li value="2">
|
||||||
<li value="1">
|
<span data-lexical-text="true">B</span>
|
||||||
<span data-lexical-text="true">A</span>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
<li value="2">
|
</li>
|
||||||
<span data-lexical-text="true">B</span>
|
</ul>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -746,71 +716,61 @@ describe('LexicalListItemNode tests', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
testEnv.outerHTML,
|
testEnv.innerHTML,
|
||||||
html`
|
html`
|
||||||
<div
|
<ul>
|
||||||
contenteditable="true"
|
<li value="1" style="list-style: none;">
|
||||||
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
|
<ul>
|
||||||
data-lexical-editor="true">
|
<li value="1">
|
||||||
<ul>
|
<span data-lexical-text="true">A1</span>
|
||||||
<li value="1">
|
</li>
|
||||||
<ul>
|
<li value="2" style="list-style: none;">
|
||||||
<li value="1">
|
<ul>
|
||||||
<span data-lexical-text="true">A1</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">A2</span>
|
||||||
<li value="2">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">A2</span>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">x</span>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
<li value="2" style="list-style: none;">
|
||||||
<li value="1">
|
<ul>
|
||||||
<span data-lexical-text="true">x</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">B</span>
|
||||||
<li value="2">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">B</span>
|
</ul>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await editor.update(() => x.remove());
|
await editor.update(() => x.remove());
|
||||||
|
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
testEnv.outerHTML,
|
testEnv.innerHTML,
|
||||||
html`
|
html`
|
||||||
<div
|
<ul>
|
||||||
contenteditable="true"
|
<li value="1" style="list-style: none;">
|
||||||
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
|
<ul>
|
||||||
data-lexical-editor="true">
|
<li value="1">
|
||||||
<ul>
|
<span data-lexical-text="true">A1</span>
|
||||||
<li value="1">
|
</li>
|
||||||
<ul>
|
<li value="2" style="list-style: none;">
|
||||||
<li value="1">
|
<ul>
|
||||||
<span data-lexical-text="true">A1</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">A2</span>
|
||||||
<li value="2">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">A2</span>
|
<li value="2">
|
||||||
</li>
|
<span data-lexical-text="true">B</span>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
<li value="2">
|
</li>
|
||||||
<span data-lexical-text="true">B</span>
|
</ul>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -856,71 +816,61 @@ describe('LexicalListItemNode tests', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
testEnv.outerHTML,
|
testEnv.innerHTML,
|
||||||
html`
|
html`
|
||||||
<div
|
<ul>
|
||||||
contenteditable="true"
|
<li value="1" style="list-style: none;">
|
||||||
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
|
<ul>
|
||||||
data-lexical-editor="true">
|
<li value="1">
|
||||||
<ul>
|
<span data-lexical-text="true">A</span>
|
||||||
<li value="1">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">A</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">x</span>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
<li value="2" style="list-style: none;">
|
||||||
<li value="1">
|
<ul>
|
||||||
<span data-lexical-text="true">x</span>
|
<li value="1" style="list-style: none;">
|
||||||
</li>
|
<ul>
|
||||||
<li value="2">
|
<li value="1">
|
||||||
<ul>
|
<span data-lexical-text="true">B1</span>
|
||||||
<li value="1">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">B1</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">B2</span>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">B2</span>
|
</ul>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await editor.update(() => x.remove());
|
await editor.update(() => x.remove());
|
||||||
|
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
testEnv.outerHTML,
|
testEnv.innerHTML,
|
||||||
html`
|
html`
|
||||||
<div
|
<ul>
|
||||||
contenteditable="true"
|
<li value="1" style="list-style: none;">
|
||||||
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
|
<ul>
|
||||||
data-lexical-editor="true">
|
<li value="1">
|
||||||
<ul>
|
<span data-lexical-text="true">A</span>
|
||||||
<li value="1">
|
</li>
|
||||||
<ul>
|
<li value="2" style="list-style: none;">
|
||||||
<li value="1">
|
<ul>
|
||||||
<span data-lexical-text="true">A</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">B1</span>
|
||||||
<li value="2">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">B1</span>
|
<li value="2">
|
||||||
</li>
|
<span data-lexical-text="true">B2</span>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
<li value="2">
|
</li>
|
||||||
<span data-lexical-text="true">B2</span>
|
</ul>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -974,81 +924,71 @@ describe('LexicalListItemNode tests', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
testEnv.outerHTML,
|
testEnv.innerHTML,
|
||||||
html`
|
html`
|
||||||
<div
|
<ul>
|
||||||
contenteditable="true"
|
<li value="1" style="list-style: none;">
|
||||||
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
|
<ul>
|
||||||
data-lexical-editor="true">
|
<li value="1">
|
||||||
<ul>
|
<span data-lexical-text="true">A1</span>
|
||||||
<li value="1">
|
</li>
|
||||||
<ul>
|
<li value="2" style="list-style: none;">
|
||||||
<li value="1">
|
<ul>
|
||||||
<span data-lexical-text="true">A1</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">A2</span>
|
||||||
<li value="2">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">A2</span>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">x</span>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
<li value="2" style="list-style: none;">
|
||||||
<li value="1">
|
<ul>
|
||||||
<span data-lexical-text="true">x</span>
|
<li value="1" style="list-style: none;">
|
||||||
</li>
|
<ul>
|
||||||
<li value="2">
|
<li value="1">
|
||||||
<ul>
|
<span data-lexical-text="true">B1</span>
|
||||||
<li value="1">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">B1</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">B2</span>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
<li value="1">
|
</li>
|
||||||
<span data-lexical-text="true">B2</span>
|
</ul>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await editor.update(() => x.remove());
|
await editor.update(() => x.remove());
|
||||||
|
|
||||||
expectHtmlToBeEqual(
|
expectHtmlToBeEqual(
|
||||||
testEnv.outerHTML,
|
testEnv.innerHTML,
|
||||||
html`
|
html`
|
||||||
<div
|
<ul>
|
||||||
contenteditable="true"
|
<li value="1" style="list-style: none;">
|
||||||
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
|
<ul>
|
||||||
data-lexical-editor="true">
|
<li value="1">
|
||||||
<ul>
|
<span data-lexical-text="true">A1</span>
|
||||||
<li value="1">
|
</li>
|
||||||
<ul>
|
<li value="2" style="list-style: none;">
|
||||||
<li value="1">
|
<ul>
|
||||||
<span data-lexical-text="true">A1</span>
|
<li value="1">
|
||||||
</li>
|
<span data-lexical-text="true">A2</span>
|
||||||
<li value="2">
|
</li>
|
||||||
<ul>
|
<li value="2">
|
||||||
<li value="1">
|
<span data-lexical-text="true">B1</span>
|
||||||
<span data-lexical-text="true">A2</span>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
<li value="2">
|
</li>
|
||||||
<span data-lexical-text="true">B1</span>
|
<li value="2">
|
||||||
</li>
|
<span data-lexical-text="true">B2</span>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
<li value="2">
|
</li>
|
||||||
<span data-lexical-text="true">B2</span>
|
</ul>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1265,99 +1205,5 @@ describe('LexicalListItemNode tests', () => {
|
||||||
expect($isListItemNode(listItemNode)).toBe(true);
|
expect($isListItemNode(listItemNode)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ListItemNode.setIndent()', () => {
|
|
||||||
let listNode: ListNode;
|
|
||||||
let listItemNode1: ListItemNode;
|
|
||||||
let listItemNode2: ListItemNode;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const {editor} = testEnv;
|
|
||||||
|
|
||||||
await editor.update(() => {
|
|
||||||
const root = $getRoot();
|
|
||||||
listNode = new ListNode('bullet', 1);
|
|
||||||
listItemNode1 = new ListItemNode();
|
|
||||||
|
|
||||||
listItemNode2 = new ListItemNode();
|
|
||||||
|
|
||||||
root.append(listNode);
|
|
||||||
listNode.append(listItemNode1, listItemNode2);
|
|
||||||
listItemNode1.append(new TextNode('one'));
|
|
||||||
listItemNode2.append(new TextNode('two'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('indents and outdents list item', async () => {
|
|
||||||
const {editor} = testEnv;
|
|
||||||
|
|
||||||
await editor.update(() => {
|
|
||||||
listItemNode1.setIndent(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
await editor.update(() => {
|
|
||||||
expect(listItemNode1.getIndent()).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
expectHtmlToBeEqual(
|
|
||||||
editor.getRootElement()!.innerHTML,
|
|
||||||
html`
|
|
||||||
<ul>
|
|
||||||
<li value="1">
|
|
||||||
<ul>
|
|
||||||
<li value="1">
|
|
||||||
<ul>
|
|
||||||
<li value="1">
|
|
||||||
<ul>
|
|
||||||
<li value="1">
|
|
||||||
<span data-lexical-text="true">one</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li value="1">
|
|
||||||
<span data-lexical-text="true">two</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await editor.update(() => {
|
|
||||||
listItemNode1.setIndent(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
await editor.update(() => {
|
|
||||||
expect(listItemNode1.getIndent()).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
expectHtmlToBeEqual(
|
|
||||||
editor.getRootElement()!.innerHTML,
|
|
||||||
html`
|
|
||||||
<ul>
|
|
||||||
<li value="1">
|
|
||||||
<span data-lexical-text="true">one</span>
|
|
||||||
</li>
|
|
||||||
<li value="2">
|
|
||||||
<span data-lexical-text="true">two</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles fractional indent values', async () => {
|
|
||||||
const {editor} = testEnv;
|
|
||||||
|
|
||||||
await editor.update(() => {
|
|
||||||
listItemNode1.setIndent(0.5);
|
|
||||||
});
|
|
||||||
|
|
||||||
await editor.update(() => {
|
|
||||||
expect(listItemNode1.getIndent()).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -294,24 +294,5 @@ describe('LexicalListNode tests', () => {
|
||||||
expect(bulletList.__listType).toBe('bullet');
|
expect(bulletList.__listType).toBe('bullet');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ListNode.clone() without list type (backward compatibility)', async () => {
|
|
||||||
const {editor} = testEnv;
|
|
||||||
|
|
||||||
await editor.update(() => {
|
|
||||||
const olNode = ListNode.clone({
|
|
||||||
__key: '1',
|
|
||||||
__start: 1,
|
|
||||||
__tag: 'ol',
|
|
||||||
} as unknown as ListNode);
|
|
||||||
const ulNode = ListNode.clone({
|
|
||||||
__key: '1',
|
|
||||||
__start: 1,
|
|
||||||
__tag: 'ul',
|
|
||||||
} as unknown as ListNode);
|
|
||||||
expect(olNode.__listType).toBe('number');
|
|
||||||
expect(ulNode.__listType).toBe('bullet');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -84,10 +84,6 @@ export function insertList(editor: LexicalEditor, listType: ListType): void {
|
||||||
if ($isRootOrShadowRoot(anchorNodeParent)) {
|
if ($isRootOrShadowRoot(anchorNodeParent)) {
|
||||||
anchorNode.replace(list);
|
anchorNode.replace(list);
|
||||||
const listItem = $createListItemNode();
|
const listItem = $createListItemNode();
|
||||||
if ($isElementNode(anchorNode)) {
|
|
||||||
listItem.setFormat(anchorNode.getFormatType());
|
|
||||||
listItem.setIndent(anchorNode.getIndent());
|
|
||||||
}
|
|
||||||
list.append(listItem);
|
list.append(listItem);
|
||||||
} else if ($isListItemNode(anchorNode)) {
|
} else if ($isListItemNode(anchorNode)) {
|
||||||
const parent = anchorNode.getParentOrThrow();
|
const parent = anchorNode.getParentOrThrow();
|
||||||
|
@ -157,8 +153,6 @@ function $createListOrMerge(node: ElementNode, listType: ListType): ListNode {
|
||||||
const previousSibling = node.getPreviousSibling();
|
const previousSibling = node.getPreviousSibling();
|
||||||
const nextSibling = node.getNextSibling();
|
const nextSibling = node.getNextSibling();
|
||||||
const listItem = $createListItemNode();
|
const listItem = $createListItemNode();
|
||||||
listItem.setFormat(node.getFormatType());
|
|
||||||
listItem.setIndent(node.getIndent());
|
|
||||||
append(listItem, node.getChildren());
|
append(listItem, node.getChildren());
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -9,4 +9,4 @@ Only components used, or intended to be used, were copied in at this point.
|
||||||
The original work built upon in this directory and below is under the copyright of Meta Platforms, Inc. and affiliates.
|
The original work built upon in this directory and below is under the copyright of Meta Platforms, Inc. and affiliates.
|
||||||
The original license can be seen in the [ORIGINAL-LEXICAL-LICENSE](./ORIGINAL-LEXICAL-LICENSE) file.
|
The original license can be seen in the [ORIGINAL-LEXICAL-LICENSE](./ORIGINAL-LEXICAL-LICENSE) file.
|
||||||
|
|
||||||
Files may have since been modified with modifications being under the license and copyright of the BookStack project as a whole.
|
Files may have since been added or modified with changes being under the license and copyright of the BookStack project as a whole.
|
|
@ -11,10 +11,10 @@ import type {EditorConfig} from "lexical/LexicalEditor";
|
||||||
import type {RangeSelection} from "lexical/LexicalSelection";
|
import type {RangeSelection} from "lexical/LexicalSelection";
|
||||||
import {
|
import {
|
||||||
CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
|
CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
|
||||||
SerializedCommonBlockNode,
|
|
||||||
setCommonBlockPropsFromElement,
|
setCommonBlockPropsFromElement,
|
||||||
updateElementWithCommonBlockProps
|
updateElementWithCommonBlockProps
|
||||||
} from "./_common";
|
} from "lexical/nodes/common";
|
||||||
|
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
|
||||||
|
|
||||||
export type CalloutCategory = 'info' | 'danger' | 'warning' | 'success';
|
export type CalloutCategory = 'info' | 'danger' | 'warning' | 'success';
|
||||||
|
|
|
@ -8,9 +8,9 @@ import {
|
||||||
Spread
|
Spread
|
||||||
} from "lexical";
|
} from "lexical";
|
||||||
import type {EditorConfig} from "lexical/LexicalEditor";
|
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||||
import {EditorDecoratorAdapter} from "../ui/framework/decorator";
|
import {EditorDecoratorAdapter} from "../../ui/framework/decorator";
|
||||||
import {CodeEditor} from "../../components";
|
import {CodeEditor} from "../../../components";
|
||||||
import {el} from "../utils/dom";
|
import {el} from "../../utils/dom";
|
||||||
|
|
||||||
export type SerializedCodeBlockNode = Spread<{
|
export type SerializedCodeBlockNode = Spread<{
|
||||||
language: string;
|
language: string;
|
|
@ -8,8 +8,8 @@ import {
|
||||||
EditorConfig,
|
EditorConfig,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
|
|
||||||
import {el} from "../utils/dom";
|
import {el} from "../../utils/dom";
|
||||||
import {extractDirectionFromElement} from "./_common";
|
import {extractDirectionFromElement} from "lexical/nodes/common";
|
||||||
|
|
||||||
export type SerializedDetailsNode = Spread<{
|
export type SerializedDetailsNode = Spread<{
|
||||||
id: string;
|
id: string;
|
|
@ -8,8 +8,8 @@ import {
|
||||||
Spread
|
Spread
|
||||||
} from "lexical";
|
} from "lexical";
|
||||||
import type {EditorConfig} from "lexical/LexicalEditor";
|
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||||
import {EditorDecoratorAdapter} from "../ui/framework/decorator";
|
import {EditorDecoratorAdapter} from "../../ui/framework/decorator";
|
||||||
import {el} from "../utils/dom";
|
import {el} from "../../utils/dom";
|
||||||
|
|
||||||
export type SerializedDiagramNode = Spread<{
|
export type SerializedDiagramNode = Spread<{
|
||||||
id: string;
|
id: string;
|
|
@ -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;
|
||||||
|
}
|
|
@ -6,8 +6,8 @@ import {
|
||||||
Spread
|
Spread
|
||||||
} from "lexical";
|
} from "lexical";
|
||||||
import type {EditorConfig} from "lexical/LexicalEditor";
|
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||||
import {CommonBlockAlignment, extractAlignmentFromElement} from "./_common";
|
import {CommonBlockAlignment, extractAlignmentFromElement} from "lexical/nodes/common";
|
||||||
import {$selectSingleNode} from "../utils/selection";
|
import {$selectSingleNode} from "../../utils/selection";
|
||||||
import {SerializedElementNode} from "lexical/nodes/LexicalElementNode";
|
import {SerializedElementNode} from "lexical/nodes/LexicalElementNode";
|
||||||
|
|
||||||
export interface ImageNodeOptions {
|
export interface ImageNodeOptions {
|
|
@ -8,14 +8,14 @@ import {
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
import type {EditorConfig} from "lexical/LexicalEditor";
|
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||||
|
|
||||||
import {el, setOrRemoveAttribute, sizeToPixels} from "../utils/dom";
|
import {el, setOrRemoveAttribute, sizeToPixels} from "../../utils/dom";
|
||||||
import {
|
import {
|
||||||
CommonBlockAlignment, deserializeCommonBlockNode,
|
CommonBlockAlignment, deserializeCommonBlockNode,
|
||||||
SerializedCommonBlockNode,
|
|
||||||
setCommonBlockPropsFromElement,
|
setCommonBlockPropsFromElement,
|
||||||
updateElementWithCommonBlockProps
|
updateElementWithCommonBlockProps
|
||||||
} from "./_common";
|
} from "lexical/nodes/common";
|
||||||
import {$selectSingleNode} from "../utils/selection";
|
import {$selectSingleNode} from "../../utils/selection";
|
||||||
|
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
|
||||||
|
|
||||||
export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';
|
export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';
|
||||||
export type MediaNodeSource = {
|
export type MediaNodeSource = {
|
|
@ -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};
|
||||||
|
}
|
|
@ -6,11 +6,6 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
|
||||||
$createHeadingNode,
|
|
||||||
$isHeadingNode,
|
|
||||||
HeadingNode,
|
|
||||||
} from '@lexical/rich-text';
|
|
||||||
import {
|
import {
|
||||||
$createTextNode,
|
$createTextNode,
|
||||||
$getRoot,
|
$getRoot,
|
||||||
|
@ -19,6 +14,7 @@ import {
|
||||||
RangeSelection,
|
RangeSelection,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
import {initializeUnitTest} from 'lexical/__tests__/utils';
|
import {initializeUnitTest} from 'lexical/__tests__/utils';
|
||||||
|
import {$createHeadingNode, $isHeadingNode, HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
|
||||||
|
|
||||||
const editorConfig = Object.freeze({
|
const editorConfig = Object.freeze({
|
||||||
namespace: '',
|
namespace: '',
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {$createQuoteNode, QuoteNode} from '@lexical/rich-text';
|
|
||||||
import {$createRangeSelection, $getRoot, ParagraphNode} from 'lexical';
|
import {$createRangeSelection, $getRoot, ParagraphNode} from 'lexical';
|
||||||
import {initializeUnitTest} from 'lexical/__tests__/utils';
|
import {initializeUnitTest} from 'lexical/__tests__/utils';
|
||||||
|
import {$createQuoteNode, QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
|
||||||
|
|
||||||
const editorConfig = Object.freeze({
|
const editorConfig = Object.freeze({
|
||||||
namespace: '',
|
namespace: '',
|
||||||
|
|
|
@ -8,42 +8,14 @@
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
CommandPayloadType,
|
CommandPayloadType,
|
||||||
DOMConversionMap,
|
|
||||||
DOMConversionOutput,
|
|
||||||
DOMExportOutput,
|
|
||||||
EditorConfig,
|
|
||||||
ElementFormatType,
|
ElementFormatType,
|
||||||
LexicalCommand,
|
LexicalCommand,
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
LexicalNode,
|
|
||||||
NodeKey,
|
|
||||||
ParagraphNode,
|
|
||||||
PasteCommandType,
|
PasteCommandType,
|
||||||
RangeSelection,
|
RangeSelection,
|
||||||
SerializedElementNode,
|
|
||||||
Spread,
|
|
||||||
TextFormatType,
|
TextFormatType,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
$insertDataTransferForRichText,
|
|
||||||
copyToClipboard,
|
|
||||||
} from '@lexical/clipboard';
|
|
||||||
import {
|
|
||||||
$moveCharacter,
|
|
||||||
$shouldOverrideDefaultCharacterSelection,
|
|
||||||
} from '@lexical/selection';
|
|
||||||
import {
|
|
||||||
$findMatchingParent,
|
|
||||||
$getNearestBlockElementAncestorOrThrow,
|
|
||||||
addClassNamesToElement,
|
|
||||||
isHTMLElement,
|
|
||||||
mergeRegister,
|
|
||||||
objectKlassEquals,
|
|
||||||
} from '@lexical/utils';
|
|
||||||
import {
|
|
||||||
$applyNodeReplacement,
|
|
||||||
$createParagraphNode,
|
|
||||||
$createRangeSelection,
|
$createRangeSelection,
|
||||||
$createTabNode,
|
$createTabNode,
|
||||||
$getAdjacentNode,
|
$getAdjacentNode,
|
||||||
|
@ -55,7 +27,6 @@ import {
|
||||||
$isElementNode,
|
$isElementNode,
|
||||||
$isNodeSelection,
|
$isNodeSelection,
|
||||||
$isRangeSelection,
|
$isRangeSelection,
|
||||||
$isRootNode,
|
|
||||||
$isTextNode,
|
$isTextNode,
|
||||||
$normalizeSelection__EXPERIMENTAL,
|
$normalizeSelection__EXPERIMENTAL,
|
||||||
$selectAll,
|
$selectAll,
|
||||||
|
@ -75,7 +46,6 @@ import {
|
||||||
ElementNode,
|
ElementNode,
|
||||||
FORMAT_ELEMENT_COMMAND,
|
FORMAT_ELEMENT_COMMAND,
|
||||||
FORMAT_TEXT_COMMAND,
|
FORMAT_TEXT_COMMAND,
|
||||||
INDENT_CONTENT_COMMAND,
|
|
||||||
INSERT_LINE_BREAK_COMMAND,
|
INSERT_LINE_BREAK_COMMAND,
|
||||||
INSERT_PARAGRAPH_COMMAND,
|
INSERT_PARAGRAPH_COMMAND,
|
||||||
INSERT_TAB_COMMAND,
|
INSERT_TAB_COMMAND,
|
||||||
|
@ -88,344 +58,22 @@ import {
|
||||||
KEY_DELETE_COMMAND,
|
KEY_DELETE_COMMAND,
|
||||||
KEY_ENTER_COMMAND,
|
KEY_ENTER_COMMAND,
|
||||||
KEY_ESCAPE_COMMAND,
|
KEY_ESCAPE_COMMAND,
|
||||||
OUTDENT_CONTENT_COMMAND,
|
|
||||||
PASTE_COMMAND,
|
PASTE_COMMAND,
|
||||||
REMOVE_TEXT_COMMAND,
|
REMOVE_TEXT_COMMAND,
|
||||||
SELECT_ALL_COMMAND,
|
SELECT_ALL_COMMAND,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
import caretFromPoint from 'lexical/shared/caretFromPoint';
|
|
||||||
import {
|
|
||||||
CAN_USE_BEFORE_INPUT,
|
|
||||||
IS_APPLE_WEBKIT,
|
|
||||||
IS_IOS,
|
|
||||||
IS_SAFARI,
|
|
||||||
} from 'lexical/shared/environment';
|
|
||||||
|
|
||||||
export type SerializedHeadingNode = Spread<
|
import {$insertDataTransferForRichText, copyToClipboard,} from '@lexical/clipboard';
|
||||||
{
|
import {$moveCharacter, $shouldOverrideDefaultCharacterSelection,} from '@lexical/selection';
|
||||||
tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
import {$findMatchingParent, mergeRegister, objectKlassEquals,} from '@lexical/utils';
|
||||||
},
|
import caretFromPoint from 'lexical/shared/caretFromPoint';
|
||||||
SerializedElementNode
|
import {CAN_USE_BEFORE_INPUT, IS_APPLE_WEBKIT, IS_IOS, IS_SAFARI,} from 'lexical/shared/environment';
|
||||||
>;
|
|
||||||
|
|
||||||
export const DRAG_DROP_PASTE: LexicalCommand<Array<File>> = createCommand(
|
export const DRAG_DROP_PASTE: LexicalCommand<Array<File>> = createCommand(
|
||||||
'DRAG_DROP_PASTE_FILE',
|
'DRAG_DROP_PASTE_FILE',
|
||||||
);
|
);
|
||||||
|
|
||||||
export type SerializedQuoteNode = SerializedElementNode;
|
|
||||||
|
|
||||||
/** @noInheritDoc */
|
|
||||||
export class QuoteNode extends ElementNode {
|
|
||||||
static getType(): string {
|
|
||||||
return 'quote';
|
|
||||||
}
|
|
||||||
|
|
||||||
static clone(node: QuoteNode): QuoteNode {
|
|
||||||
return new QuoteNode(node.__key);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(key?: NodeKey) {
|
|
||||||
super(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// View
|
|
||||||
|
|
||||||
createDOM(config: EditorConfig): HTMLElement {
|
|
||||||
const element = document.createElement('blockquote');
|
|
||||||
addClassNamesToElement(element, config.theme.quote);
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap | null {
|
|
||||||
return {
|
|
||||||
blockquote: (node: Node) => ({
|
|
||||||
conversion: $convertBlockquoteElement,
|
|
||||||
priority: 0,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
|
||||||
const {element} = super.exportDOM(editor);
|
|
||||||
|
|
||||||
if (element && isHTMLElement(element)) {
|
|
||||||
if (this.isEmpty()) {
|
|
||||||
element.append(document.createElement('br'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatType = this.getFormatType();
|
|
||||||
element.style.textAlign = formatType;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
element,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {
|
|
||||||
const node = $createQuoteNode();
|
|
||||||
node.setFormat(serializedNode.format);
|
|
||||||
node.setIndent(serializedNode.indent);
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
exportJSON(): SerializedElementNode {
|
|
||||||
return {
|
|
||||||
...super.exportJSON(),
|
|
||||||
type: 'quote',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutation
|
|
||||||
|
|
||||||
insertNewAfter(_: RangeSelection, restoreSelection?: boolean): ParagraphNode {
|
|
||||||
const newBlock = $createParagraphNode();
|
|
||||||
const direction = this.getDirection();
|
|
||||||
newBlock.setDirection(direction);
|
|
||||||
this.insertAfter(newBlock, restoreSelection);
|
|
||||||
return newBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
collapseAtStart(): true {
|
|
||||||
const paragraph = $createParagraphNode();
|
|
||||||
const children = this.getChildren();
|
|
||||||
children.forEach((child) => paragraph.append(child));
|
|
||||||
this.replace(paragraph);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
canMergeWhenEmpty(): true {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function $createQuoteNode(): QuoteNode {
|
|
||||||
return $applyNodeReplacement(new QuoteNode());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function $isQuoteNode(
|
|
||||||
node: LexicalNode | null | undefined,
|
|
||||||
): node is QuoteNode {
|
|
||||||
return node instanceof QuoteNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HeadingTagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
||||||
|
|
||||||
/** @noInheritDoc */
|
|
||||||
export class HeadingNode extends ElementNode {
|
|
||||||
/** @internal */
|
|
||||||
__tag: HeadingTagType;
|
|
||||||
|
|
||||||
static getType(): string {
|
|
||||||
return 'heading';
|
|
||||||
}
|
|
||||||
|
|
||||||
static clone(node: HeadingNode): HeadingNode {
|
|
||||||
return new HeadingNode(node.__tag, node.__key);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(tag: HeadingTagType, key?: NodeKey) {
|
|
||||||
super(key);
|
|
||||||
this.__tag = tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTag(): HeadingTagType {
|
|
||||||
return this.__tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
// View
|
|
||||||
|
|
||||||
createDOM(config: EditorConfig): HTMLElement {
|
|
||||||
const tag = this.__tag;
|
|
||||||
const element = document.createElement(tag);
|
|
||||||
const theme = config.theme;
|
|
||||||
const classNames = theme.heading;
|
|
||||||
if (classNames !== undefined) {
|
|
||||||
const className = classNames[tag];
|
|
||||||
addClassNamesToElement(element, className);
|
|
||||||
}
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDOM(prevNode: HeadingNode, dom: HTMLElement): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap | null {
|
|
||||||
return {
|
|
||||||
h1: (node: Node) => ({
|
|
||||||
conversion: $convertHeadingElement,
|
|
||||||
priority: 0,
|
|
||||||
}),
|
|
||||||
h2: (node: Node) => ({
|
|
||||||
conversion: $convertHeadingElement,
|
|
||||||
priority: 0,
|
|
||||||
}),
|
|
||||||
h3: (node: Node) => ({
|
|
||||||
conversion: $convertHeadingElement,
|
|
||||||
priority: 0,
|
|
||||||
}),
|
|
||||||
h4: (node: Node) => ({
|
|
||||||
conversion: $convertHeadingElement,
|
|
||||||
priority: 0,
|
|
||||||
}),
|
|
||||||
h5: (node: Node) => ({
|
|
||||||
conversion: $convertHeadingElement,
|
|
||||||
priority: 0,
|
|
||||||
}),
|
|
||||||
h6: (node: Node) => ({
|
|
||||||
conversion: $convertHeadingElement,
|
|
||||||
priority: 0,
|
|
||||||
}),
|
|
||||||
p: (node: Node) => {
|
|
||||||
// domNode is a <p> since we matched it by nodeName
|
|
||||||
const paragraph = node as HTMLParagraphElement;
|
|
||||||
const firstChild = paragraph.firstChild;
|
|
||||||
if (firstChild !== null && isGoogleDocsTitle(firstChild)) {
|
|
||||||
return {
|
|
||||||
conversion: () => ({node: null}),
|
|
||||||
priority: 3,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
span: (node: Node) => {
|
|
||||||
if (isGoogleDocsTitle(node)) {
|
|
||||||
return {
|
|
||||||
conversion: (domNode: Node) => {
|
|
||||||
return {
|
|
||||||
node: $createHeadingNode('h1'),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
priority: 3,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
|
||||||
const {element} = super.exportDOM(editor);
|
|
||||||
|
|
||||||
if (element && isHTMLElement(element)) {
|
|
||||||
if (this.isEmpty()) {
|
|
||||||
element.append(document.createElement('br'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatType = this.getFormatType();
|
|
||||||
element.style.textAlign = formatType;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
element,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
|
|
||||||
const node = $createHeadingNode(serializedNode.tag);
|
|
||||||
node.setFormat(serializedNode.format);
|
|
||||||
node.setIndent(serializedNode.indent);
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
exportJSON(): SerializedHeadingNode {
|
|
||||||
return {
|
|
||||||
...super.exportJSON(),
|
|
||||||
tag: this.getTag(),
|
|
||||||
type: 'heading',
|
|
||||||
version: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutation
|
|
||||||
insertNewAfter(
|
|
||||||
selection?: RangeSelection,
|
|
||||||
restoreSelection = true,
|
|
||||||
): ParagraphNode | HeadingNode {
|
|
||||||
const anchorOffet = selection ? selection.anchor.offset : 0;
|
|
||||||
const lastDesc = this.getLastDescendant();
|
|
||||||
const isAtEnd =
|
|
||||||
!lastDesc ||
|
|
||||||
(selection &&
|
|
||||||
selection.anchor.key === lastDesc.getKey() &&
|
|
||||||
anchorOffet === lastDesc.getTextContentSize());
|
|
||||||
const newElement =
|
|
||||||
isAtEnd || !selection
|
|
||||||
? $createParagraphNode()
|
|
||||||
: $createHeadingNode(this.getTag());
|
|
||||||
const direction = this.getDirection();
|
|
||||||
newElement.setDirection(direction);
|
|
||||||
this.insertAfter(newElement, restoreSelection);
|
|
||||||
if (anchorOffet === 0 && !this.isEmpty() && selection) {
|
|
||||||
const paragraph = $createParagraphNode();
|
|
||||||
paragraph.select();
|
|
||||||
this.replace(paragraph, true);
|
|
||||||
}
|
|
||||||
return newElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
collapseAtStart(): true {
|
|
||||||
const newElement = !this.isEmpty()
|
|
||||||
? $createHeadingNode(this.getTag())
|
|
||||||
: $createParagraphNode();
|
|
||||||
const children = this.getChildren();
|
|
||||||
children.forEach((child) => newElement.append(child));
|
|
||||||
this.replace(newElement);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
extractWithChild(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isGoogleDocsTitle(domNode: Node): boolean {
|
|
||||||
if (domNode.nodeName.toLowerCase() === 'span') {
|
|
||||||
return (domNode as HTMLSpanElement).style.fontSize === '26pt';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
|
|
||||||
const nodeName = element.nodeName.toLowerCase();
|
|
||||||
let node = null;
|
|
||||||
if (
|
|
||||||
nodeName === 'h1' ||
|
|
||||||
nodeName === 'h2' ||
|
|
||||||
nodeName === 'h3' ||
|
|
||||||
nodeName === 'h4' ||
|
|
||||||
nodeName === 'h5' ||
|
|
||||||
nodeName === 'h6'
|
|
||||||
) {
|
|
||||||
node = $createHeadingNode(nodeName);
|
|
||||||
if (element.style !== null) {
|
|
||||||
node.setFormat(element.style.textAlign as ElementFormatType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {node};
|
|
||||||
}
|
|
||||||
|
|
||||||
function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
|
|
||||||
const node = $createQuoteNode();
|
|
||||||
if (element.style !== null) {
|
|
||||||
node.setFormat(element.style.textAlign as ElementFormatType);
|
|
||||||
}
|
|
||||||
return {node};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode {
|
|
||||||
return $applyNodeReplacement(new HeadingNode(headingTag));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function $isHeadingNode(
|
|
||||||
node: LexicalNode | null | undefined,
|
|
||||||
): node is HeadingNode {
|
|
||||||
return node instanceof HeadingNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPasteForRichText(
|
function onPasteForRichText(
|
||||||
event: CommandPayloadType<typeof PASTE_COMMAND>,
|
event: CommandPayloadType<typeof PASTE_COMMAND>,
|
||||||
|
@ -651,9 +299,6 @@ export function registerRichText(editor: LexicalEditor): () => void {
|
||||||
(parentNode): parentNode is ElementNode =>
|
(parentNode): parentNode is ElementNode =>
|
||||||
$isElementNode(parentNode) && !parentNode.isInline(),
|
$isElementNode(parentNode) && !parentNode.isInline(),
|
||||||
);
|
);
|
||||||
if (element !== null) {
|
|
||||||
element.setFormat(format);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
@ -691,28 +336,6 @@ export function registerRichText(editor: LexicalEditor): () => void {
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
),
|
),
|
||||||
editor.registerCommand(
|
|
||||||
INDENT_CONTENT_COMMAND,
|
|
||||||
() => {
|
|
||||||
return $handleIndentAndOutdent((block) => {
|
|
||||||
const indent = block.getIndent();
|
|
||||||
block.setIndent(indent + 1);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
COMMAND_PRIORITY_EDITOR,
|
|
||||||
),
|
|
||||||
editor.registerCommand(
|
|
||||||
OUTDENT_CONTENT_COMMAND,
|
|
||||||
() => {
|
|
||||||
return $handleIndentAndOutdent((block) => {
|
|
||||||
const indent = block.getIndent();
|
|
||||||
if (indent > 0) {
|
|
||||||
block.setIndent(indent - 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
COMMAND_PRIORITY_EDITOR,
|
|
||||||
),
|
|
||||||
editor.registerCommand<KeyboardEvent>(
|
editor.registerCommand<KeyboardEvent>(
|
||||||
KEY_ARROW_UP_COMMAND,
|
KEY_ARROW_UP_COMMAND,
|
||||||
(event) => {
|
(event) => {
|
||||||
|
@ -846,19 +469,7 @@ export function registerRichText(editor: LexicalEditor): () => void {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const {anchor} = selection;
|
|
||||||
const anchorNode = anchor.getNode();
|
|
||||||
|
|
||||||
if (
|
|
||||||
selection.isCollapsed() &&
|
|
||||||
anchor.offset === 0 &&
|
|
||||||
!$isRootNode(anchorNode)
|
|
||||||
) {
|
|
||||||
const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
|
|
||||||
if (element.getIndent() > 0) {
|
|
||||||
return editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true);
|
return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true);
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import {$createLinkNode} from '@lexical/link';
|
import {$createLinkNode} from '@lexical/link';
|
||||||
import {$createListItemNode, $createListNode} from '@lexical/list';
|
import {$createListItemNode, $createListNode} from '@lexical/list';
|
||||||
import {$createHeadingNode, registerRichText} from '@lexical/rich-text';
|
import {registerRichText} from '@lexical/rich-text';
|
||||||
import {
|
import {
|
||||||
$addNodeStyle,
|
$addNodeStyle,
|
||||||
$getSelectionStyleValueForProperty,
|
$getSelectionStyleValueForProperty,
|
||||||
|
@ -74,6 +74,7 @@ import {
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import {createEmptyHistoryState, registerHistory} from "@lexical/history";
|
import {createEmptyHistoryState, registerHistory} from "@lexical/history";
|
||||||
import {mergeRegister} from "@lexical/utils";
|
import {mergeRegister} from "@lexical/utils";
|
||||||
|
import {$createHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
|
||||||
|
|
||||||
interface ExpectedSelection {
|
interface ExpectedSelection {
|
||||||
anchorPath: number[];
|
anchorPath: number[];
|
||||||
|
@ -2604,7 +2605,7 @@ describe('LexicalSelection tests', () => {
|
||||||
return $createHeadingNode('h1');
|
return $createHeadingNode('h1');
|
||||||
});
|
});
|
||||||
expect(JSON.stringify(testEditor._pendingEditorState?.toJSON())).toBe(
|
expect(JSON.stringify(testEditor._pendingEditorState?.toJSON())).toBe(
|
||||||
'{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[{"children":[{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":3,"rowSpan":1},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1}],"direction":null,"format":"","indent":0,"type":"tablerow","version":1}],"direction":null,"format":"","indent":0,"type":"table","version":1},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"}],"direction":null,"format":"","indent":0,"type":"root","version":1}}',
|
'{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"},{"children":[{"children":[{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"},{"children":[],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"},{"children":[],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"}],"direction":null,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":3,"rowSpan":1,"styles":{},"alignment":""},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"},{"children":[],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"},{"children":[],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"}],"direction":null,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1,"styles":{},"alignment":""}],"direction":null,"type":"tablerow","version":1,"styles":{},"height":0}],"direction":null,"type":"table","version":1,"id":"","alignment":"","inset":0,"colWidths":[],"styles":{}},{"children":[],"direction":null,"type":"heading","version":1,"id":"","alignment":"","inset":0,"tag":"h1"}],"direction":null,"type":"root","version":1}}',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2694,7 +2695,7 @@ describe('LexicalSelection tests', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(element.innerHTML).toStrictEqual(
|
expect(element.innerHTML).toStrictEqual(
|
||||||
`<h1><span data-lexical-text="true">1</span></h1><h1 style="padding-inline-start: calc(1 * 40px);"><span data-lexical-text="true">1.1</span></h1>`,
|
`<h1><span data-lexical-text="true">1</span></h1><ul><li value="1"><h1><span data-lexical-text="true">1.1</span></h1></li></ul>`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2733,7 +2734,7 @@ describe('LexicalSelection tests', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(element.innerHTML).toStrictEqual(
|
expect(element.innerHTML).toStrictEqual(
|
||||||
`<h1 style="padding-inline-start: calc(1 * 40px);"><span data-lexical-text="true">1.1</span></h1>`,
|
`<ul><li value="1"><h1><span data-lexical-text="true">1.1</span></h1></li></ul>`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {$createLinkNode} from '@lexical/link';
|
import {$createLinkNode} from '@lexical/link';
|
||||||
import {$createHeadingNode, $isHeadingNode} from '@lexical/rich-text';
|
|
||||||
import {
|
import {
|
||||||
$getSelectionStyleValueForProperty,
|
$getSelectionStyleValueForProperty,
|
||||||
$patchStyleText,
|
$patchStyleText,
|
||||||
|
@ -44,6 +43,7 @@ import {
|
||||||
} from 'lexical/__tests__/utils';
|
} from 'lexical/__tests__/utils';
|
||||||
|
|
||||||
import {$setAnchorPoint, $setFocusPoint} from '../utils';
|
import {$setAnchorPoint, $setFocusPoint} from '../utils';
|
||||||
|
import {$createHeadingNode, $isHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
|
||||||
|
|
||||||
Range.prototype.getBoundingClientRect = function (): DOMRect {
|
Range.prototype.getBoundingClientRect = function (): DOMRect {
|
||||||
const rect = {
|
const rect = {
|
||||||
|
|
|
@ -81,8 +81,6 @@ export function $setBlocksType(
|
||||||
invariant($isElementNode(node), 'Expected block node to be an ElementNode');
|
invariant($isElementNode(node), 'Expected block node to be an ElementNode');
|
||||||
|
|
||||||
const targetElement = createElement();
|
const targetElement = createElement();
|
||||||
targetElement.setFormat(node.getFormatType());
|
|
||||||
targetElement.setIndent(node.getIndent());
|
|
||||||
node.replace(targetElement, true);
|
node.replace(targetElement, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,8 +134,6 @@ export function $wrapNodes(
|
||||||
: anchor.getNode();
|
: anchor.getNode();
|
||||||
const children = target.getChildren();
|
const children = target.getChildren();
|
||||||
let element = createElement();
|
let element = createElement();
|
||||||
element.setFormat(target.getFormatType());
|
|
||||||
element.setIndent(target.getIndent());
|
|
||||||
children.forEach((child) => element.append(child));
|
children.forEach((child) => element.append(child));
|
||||||
|
|
||||||
if (wrappingElement) {
|
if (wrappingElement) {
|
||||||
|
@ -277,8 +273,6 @@ export function $wrapNodesImpl(
|
||||||
|
|
||||||
if (elementMapping.get(parentKey) === undefined) {
|
if (elementMapping.get(parentKey) === undefined) {
|
||||||
const targetElement = createElement();
|
const targetElement = createElement();
|
||||||
targetElement.setFormat(parent.getFormatType());
|
|
||||||
targetElement.setIndent(parent.getIndent());
|
|
||||||
elements.push(targetElement);
|
elements.push(targetElement);
|
||||||
elementMapping.set(parentKey, targetElement);
|
elementMapping.set(parentKey, targetElement);
|
||||||
// Move node and its siblings to the new
|
// Move node and its siblings to the new
|
||||||
|
@ -299,8 +293,6 @@ export function $wrapNodesImpl(
|
||||||
'Expected node in emptyElements to be an ElementNode',
|
'Expected node in emptyElements to be an ElementNode',
|
||||||
);
|
);
|
||||||
const targetElement = createElement();
|
const targetElement = createElement();
|
||||||
targetElement.setFormat(node.getFormatType());
|
|
||||||
targetElement.setIndent(node.getIndent());
|
|
||||||
elements.push(targetElement);
|
elements.push(targetElement);
|
||||||
node.remove(true);
|
node.remove(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,8 @@ import {
|
||||||
ElementNode,
|
ElementNode,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
|
|
||||||
import {COLUMN_WIDTH, PIXEL_VALUE_REG_EXP} from './constants';
|
import {extractStyleMapFromElement, StyleMap} from "../../utils/dom";
|
||||||
|
import {CommonBlockAlignment, extractAlignmentFromElement} from "lexical/nodes/common";
|
||||||
|
|
||||||
export const TableCellHeaderStates = {
|
export const TableCellHeaderStates = {
|
||||||
BOTH: 3,
|
BOTH: 3,
|
||||||
|
@ -47,6 +48,8 @@ export type SerializedTableCellNode = Spread<
|
||||||
headerState: TableCellHeaderState;
|
headerState: TableCellHeaderState;
|
||||||
width?: number;
|
width?: number;
|
||||||
backgroundColor?: null | string;
|
backgroundColor?: null | string;
|
||||||
|
styles: Record<string, string>;
|
||||||
|
alignment: CommonBlockAlignment;
|
||||||
},
|
},
|
||||||
SerializedElementNode
|
SerializedElementNode
|
||||||
>;
|
>;
|
||||||
|
@ -63,6 +66,10 @@ export class TableCellNode extends ElementNode {
|
||||||
__width?: number;
|
__width?: number;
|
||||||
/** @internal */
|
/** @internal */
|
||||||
__backgroundColor: null | string;
|
__backgroundColor: null | string;
|
||||||
|
/** @internal */
|
||||||
|
__styles: StyleMap = new Map;
|
||||||
|
/** @internal */
|
||||||
|
__alignment: CommonBlockAlignment = '';
|
||||||
|
|
||||||
static getType(): string {
|
static getType(): string {
|
||||||
return 'tablecell';
|
return 'tablecell';
|
||||||
|
@ -77,6 +84,8 @@ export class TableCellNode extends ElementNode {
|
||||||
);
|
);
|
||||||
cellNode.__rowSpan = node.__rowSpan;
|
cellNode.__rowSpan = node.__rowSpan;
|
||||||
cellNode.__backgroundColor = node.__backgroundColor;
|
cellNode.__backgroundColor = node.__backgroundColor;
|
||||||
|
cellNode.__styles = new Map(node.__styles);
|
||||||
|
cellNode.__alignment = node.__alignment;
|
||||||
return cellNode;
|
return cellNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,16 +103,20 @@ export class TableCellNode extends ElementNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedTableCellNode): TableCellNode {
|
static importJSON(serializedNode: SerializedTableCellNode): TableCellNode {
|
||||||
const colSpan = serializedNode.colSpan || 1;
|
const node = $createTableCellNode(
|
||||||
const rowSpan = serializedNode.rowSpan || 1;
|
serializedNode.headerState,
|
||||||
const cellNode = $createTableCellNode(
|
serializedNode.colSpan,
|
||||||
serializedNode.headerState,
|
serializedNode.width,
|
||||||
colSpan,
|
|
||||||
serializedNode.width || undefined,
|
|
||||||
);
|
);
|
||||||
cellNode.__rowSpan = rowSpan;
|
|
||||||
cellNode.__backgroundColor = serializedNode.backgroundColor || null;
|
if (serializedNode.rowSpan) {
|
||||||
return cellNode;
|
node.setRowSpan(serializedNode.rowSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
node.setStyles(new Map(Object.entries(serializedNode.styles)));
|
||||||
|
node.setAlignment(serializedNode.alignment);
|
||||||
|
|
||||||
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -144,34 +157,19 @@ export class TableCellNode extends ElementNode {
|
||||||
this.hasHeader() && config.theme.tableCellHeader,
|
this.hasHeader() && config.theme.tableCellHeader,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for (const [name, value] of this.__styles.entries()) {
|
||||||
|
element.style.setProperty(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.__alignment) {
|
||||||
|
element.classList.add('align-' + this.__alignment);
|
||||||
|
}
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
||||||
const {element} = super.exportDOM(editor);
|
const {element} = super.exportDOM(editor);
|
||||||
|
|
||||||
if (element) {
|
|
||||||
const element_ = element as HTMLTableCellElement;
|
|
||||||
element_.style.border = '1px solid black';
|
|
||||||
if (this.__colSpan > 1) {
|
|
||||||
element_.colSpan = this.__colSpan;
|
|
||||||
}
|
|
||||||
if (this.__rowSpan > 1) {
|
|
||||||
element_.rowSpan = this.__rowSpan;
|
|
||||||
}
|
|
||||||
element_.style.width = `${this.getWidth() || COLUMN_WIDTH}px`;
|
|
||||||
|
|
||||||
element_.style.verticalAlign = 'top';
|
|
||||||
element_.style.textAlign = 'start';
|
|
||||||
|
|
||||||
const backgroundColor = this.getBackgroundColor();
|
|
||||||
if (backgroundColor !== null) {
|
|
||||||
element_.style.backgroundColor = backgroundColor;
|
|
||||||
} else if (this.hasHeader()) {
|
|
||||||
element_.style.backgroundColor = '#f2f3f5';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
element,
|
element,
|
||||||
};
|
};
|
||||||
|
@ -186,6 +184,8 @@ export class TableCellNode extends ElementNode {
|
||||||
rowSpan: this.__rowSpan,
|
rowSpan: this.__rowSpan,
|
||||||
type: 'tablecell',
|
type: 'tablecell',
|
||||||
width: this.getWidth(),
|
width: this.getWidth(),
|
||||||
|
styles: Object.fromEntries(this.__styles),
|
||||||
|
alignment: this.__alignment,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,6 +231,38 @@ export class TableCellNode extends ElementNode {
|
||||||
return this.getLatest().__width;
|
return this.getLatest().__width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearWidth(): void {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__width = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStyles(): StyleMap {
|
||||||
|
const self = this.getLatest();
|
||||||
|
return new Map(self.__styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStyles(styles: StyleMap): void {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__styles = new Map(styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAlignment(alignment: CommonBlockAlignment) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__alignment = alignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAlignment(): CommonBlockAlignment {
|
||||||
|
const self = this.getLatest();
|
||||||
|
return self.__alignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTag(tag: string): void {
|
||||||
|
const isHeader = tag.toLowerCase() === 'th';
|
||||||
|
const state = isHeader ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS;
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__headerState = state;
|
||||||
|
}
|
||||||
|
|
||||||
getBackgroundColor(): null | string {
|
getBackgroundColor(): null | string {
|
||||||
return this.getLatest().__backgroundColor;
|
return this.getLatest().__backgroundColor;
|
||||||
}
|
}
|
||||||
|
@ -265,7 +297,9 @@ export class TableCellNode extends ElementNode {
|
||||||
prevNode.__width !== this.__width ||
|
prevNode.__width !== this.__width ||
|
||||||
prevNode.__colSpan !== this.__colSpan ||
|
prevNode.__colSpan !== this.__colSpan ||
|
||||||
prevNode.__rowSpan !== this.__rowSpan ||
|
prevNode.__rowSpan !== this.__rowSpan ||
|
||||||
prevNode.__backgroundColor !== this.__backgroundColor
|
prevNode.__backgroundColor !== this.__backgroundColor ||
|
||||||
|
prevNode.__styles !== this.__styles ||
|
||||||
|
prevNode.__alignment !== this.__alignment
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,38 +321,42 @@ export class TableCellNode extends ElementNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $convertTableCellNodeElement(
|
export function $convertTableCellNodeElement(
|
||||||
domNode: Node,
|
domNode: Node,
|
||||||
): DOMConversionOutput {
|
): DOMConversionOutput {
|
||||||
const domNode_ = domNode as HTMLTableCellElement;
|
const domNode_ = domNode as HTMLTableCellElement;
|
||||||
const nodeName = domNode.nodeName.toLowerCase();
|
const nodeName = domNode.nodeName.toLowerCase();
|
||||||
|
|
||||||
let width: number | undefined = undefined;
|
let width: number | undefined = undefined;
|
||||||
|
|
||||||
|
|
||||||
|
const PIXEL_VALUE_REG_EXP = /^(\d+(?:\.\d+)?)px$/;
|
||||||
if (PIXEL_VALUE_REG_EXP.test(domNode_.style.width)) {
|
if (PIXEL_VALUE_REG_EXP.test(domNode_.style.width)) {
|
||||||
width = parseFloat(domNode_.style.width);
|
width = parseFloat(domNode_.style.width);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableCellNode = $createTableCellNode(
|
const tableCellNode = $createTableCellNode(
|
||||||
nodeName === 'th'
|
nodeName === 'th'
|
||||||
? TableCellHeaderStates.ROW
|
? TableCellHeaderStates.ROW
|
||||||
: TableCellHeaderStates.NO_STATUS,
|
: TableCellHeaderStates.NO_STATUS,
|
||||||
domNode_.colSpan,
|
domNode_.colSpan,
|
||||||
width,
|
width,
|
||||||
);
|
);
|
||||||
|
|
||||||
tableCellNode.__rowSpan = domNode_.rowSpan;
|
tableCellNode.__rowSpan = domNode_.rowSpan;
|
||||||
const backgroundColor = domNode_.style.backgroundColor;
|
|
||||||
if (backgroundColor !== '') {
|
|
||||||
tableCellNode.__backgroundColor = backgroundColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
const style = domNode_.style;
|
const style = domNode_.style;
|
||||||
const textDecoration = style.textDecoration.split(' ');
|
const textDecoration = style.textDecoration.split(' ');
|
||||||
const hasBoldFontWeight =
|
const hasBoldFontWeight =
|
||||||
style.fontWeight === '700' || style.fontWeight === 'bold';
|
style.fontWeight === '700' || style.fontWeight === 'bold';
|
||||||
const hasLinethroughTextDecoration = textDecoration.includes('line-through');
|
const hasLinethroughTextDecoration = textDecoration.includes('line-through');
|
||||||
const hasItalicFontStyle = style.fontStyle === 'italic';
|
const hasItalicFontStyle = style.fontStyle === 'italic';
|
||||||
const hasUnderlineTextDecoration = textDecoration.includes('underline');
|
const hasUnderlineTextDecoration = textDecoration.includes('underline');
|
||||||
|
|
||||||
|
if (domNode instanceof HTMLElement) {
|
||||||
|
tableCellNode.setStyles(extractStyleMapFromElement(domNode));
|
||||||
|
tableCellNode.setAlignment(extractAlignmentFromElement(domNode));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
after: (childLexicalNodes) => {
|
after: (childLexicalNodes) => {
|
||||||
if (childLexicalNodes.length === 0) {
|
if (childLexicalNodes.length === 0) {
|
||||||
|
@ -330,8 +368,8 @@ export function $convertTableCellNodeElement(
|
||||||
if ($isTableCellNode(parentLexicalNode) && !$isElementNode(lexicalNode)) {
|
if ($isTableCellNode(parentLexicalNode) && !$isElementNode(lexicalNode)) {
|
||||||
const paragraphNode = $createParagraphNode();
|
const paragraphNode = $createParagraphNode();
|
||||||
if (
|
if (
|
||||||
$isLineBreakNode(lexicalNode) &&
|
$isLineBreakNode(lexicalNode) &&
|
||||||
lexicalNode.getTextContent() === '\n'
|
lexicalNode.getTextContent() === '\n'
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -360,7 +398,7 @@ export function $convertTableCellNodeElement(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createTableCellNode(
|
export function $createTableCellNode(
|
||||||
headerState: TableCellHeaderState,
|
headerState: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,
|
||||||
colSpan = 1,
|
colSpan = 1,
|
||||||
width?: number,
|
width?: number,
|
||||||
): TableCellNode {
|
): TableCellNode {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {TableCellNode} from './LexicalTableCellNode';
|
import type {TableCellNode} from './LexicalTableCellNode';
|
||||||
import type {
|
import {
|
||||||
DOMConversionMap,
|
DOMConversionMap,
|
||||||
DOMConversionOutput,
|
DOMConversionOutput,
|
||||||
DOMExportOutput,
|
DOMExportOutput,
|
||||||
|
@ -15,31 +15,48 @@ import type {
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
LexicalNode,
|
LexicalNode,
|
||||||
NodeKey,
|
NodeKey,
|
||||||
SerializedElementNode,
|
Spread,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
|
|
||||||
import {addClassNamesToElement, isHTMLElement} from '@lexical/utils';
|
import {addClassNamesToElement, isHTMLElement} from '@lexical/utils';
|
||||||
import {
|
import {
|
||||||
$applyNodeReplacement,
|
$applyNodeReplacement,
|
||||||
$getNearestNodeFromDOMNode,
|
$getNearestNodeFromDOMNode,
|
||||||
ElementNode,
|
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
|
|
||||||
import {$isTableCellNode} from './LexicalTableCellNode';
|
import {$isTableCellNode} from './LexicalTableCellNode';
|
||||||
import {TableDOMCell, TableDOMTable} from './LexicalTableObserver';
|
import {TableDOMCell, TableDOMTable} from './LexicalTableObserver';
|
||||||
import {$isTableRowNode, TableRowNode} from './LexicalTableRowNode';
|
|
||||||
import {getTable} from './LexicalTableSelectionHelpers';
|
import {getTable} from './LexicalTableSelectionHelpers';
|
||||||
|
import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
|
||||||
|
import {
|
||||||
|
commonPropertiesDifferent, deserializeCommonBlockNode,
|
||||||
|
setCommonBlockPropsFromElement,
|
||||||
|
updateElementWithCommonBlockProps
|
||||||
|
} from "lexical/nodes/common";
|
||||||
|
import {el, extractStyleMapFromElement, StyleMap} from "../../utils/dom";
|
||||||
|
import {getTableColumnWidths} from "../../utils/tables";
|
||||||
|
|
||||||
export type SerializedTableNode = SerializedElementNode;
|
export type SerializedTableNode = Spread<{
|
||||||
|
colWidths: string[];
|
||||||
|
styles: Record<string, string>,
|
||||||
|
}, SerializedCommonBlockNode>
|
||||||
|
|
||||||
/** @noInheritDoc */
|
/** @noInheritDoc */
|
||||||
export class TableNode extends ElementNode {
|
export class TableNode extends CommonBlockNode {
|
||||||
|
__colWidths: string[] = [];
|
||||||
|
__styles: StyleMap = new Map;
|
||||||
|
|
||||||
static getType(): string {
|
static getType(): string {
|
||||||
return 'table';
|
return 'table';
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: TableNode): TableNode {
|
static clone(node: TableNode): TableNode {
|
||||||
return new TableNode(node.__key);
|
const newNode = new TableNode(node.__key);
|
||||||
|
copyCommonBlockProperties(node, newNode);
|
||||||
|
newNode.__colWidths = node.__colWidths;
|
||||||
|
newNode.__styles = new Map(node.__styles);
|
||||||
|
return newNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap | null {
|
static importDOM(): DOMConversionMap | null {
|
||||||
|
@ -52,18 +69,24 @@ export class TableNode extends ElementNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(_serializedNode: SerializedTableNode): TableNode {
|
static importJSON(_serializedNode: SerializedTableNode): TableNode {
|
||||||
return $createTableNode();
|
const node = $createTableNode();
|
||||||
|
deserializeCommonBlockNode(_serializedNode, node);
|
||||||
|
node.setColWidths(_serializedNode.colWidths);
|
||||||
|
node.setStyles(new Map(Object.entries(_serializedNode.styles)));
|
||||||
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(key?: NodeKey) {
|
constructor(key?: NodeKey) {
|
||||||
super(key);
|
super(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
exportJSON(): SerializedElementNode {
|
exportJSON(): SerializedTableNode {
|
||||||
return {
|
return {
|
||||||
...super.exportJSON(),
|
...super.exportJSON(),
|
||||||
type: 'table',
|
type: 'table',
|
||||||
version: 1,
|
version: 1,
|
||||||
|
colWidths: this.__colWidths,
|
||||||
|
styles: Object.fromEntries(this.__styles),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,11 +95,33 @@ export class TableNode extends ElementNode {
|
||||||
|
|
||||||
addClassNamesToElement(tableElement, config.theme.table);
|
addClassNamesToElement(tableElement, config.theme.table);
|
||||||
|
|
||||||
|
updateElementWithCommonBlockProps(tableElement, this);
|
||||||
|
|
||||||
|
const colWidths = this.getColWidths();
|
||||||
|
if (colWidths.length > 0) {
|
||||||
|
const colgroup = el('colgroup');
|
||||||
|
for (const width of colWidths) {
|
||||||
|
const col = el('col');
|
||||||
|
if (width) {
|
||||||
|
col.style.width = width;
|
||||||
|
}
|
||||||
|
colgroup.append(col);
|
||||||
|
}
|
||||||
|
tableElement.append(colgroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, value] of this.__styles.entries()) {
|
||||||
|
tableElement.style.setProperty(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
return tableElement;
|
return tableElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDOM(): boolean {
|
updateDOM(_prevNode: TableNode): boolean {
|
||||||
return false;
|
return commonPropertiesDifferent(_prevNode, this)
|
||||||
|
|| this.__colWidths.join(':') !== _prevNode.__colWidths.join(':')
|
||||||
|
|| this.__styles.size !== _prevNode.__styles.size
|
||||||
|
|| (Array.from(this.__styles.values()).join(':') !== (Array.from(_prevNode.__styles.values()).join(':')));
|
||||||
}
|
}
|
||||||
|
|
||||||
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
||||||
|
@ -115,6 +160,26 @@ export class TableNode extends ElementNode {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setColWidths(widths: string[]) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__colWidths = widths;
|
||||||
|
}
|
||||||
|
|
||||||
|
getColWidths(): string[] {
|
||||||
|
const self = this.getLatest();
|
||||||
|
return self.__colWidths;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStyles(): StyleMap {
|
||||||
|
const self = this.getLatest();
|
||||||
|
return new Map(self.__styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStyles(styles: StyleMap): void {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__styles = new Map(styles);
|
||||||
|
}
|
||||||
|
|
||||||
getCordsFromCellNode(
|
getCordsFromCellNode(
|
||||||
tableCellNode: TableCellNode,
|
tableCellNode: TableCellNode,
|
||||||
table: TableDOMTable,
|
table: TableDOMTable,
|
||||||
|
@ -239,8 +304,15 @@ export function $getElementForTableNode(
|
||||||
return getTable(tableElement);
|
return getTable(tableElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $convertTableElement(_domNode: Node): DOMConversionOutput {
|
export function $convertTableElement(element: HTMLElement): DOMConversionOutput {
|
||||||
return {node: $createTableNode()};
|
const node = $createTableNode();
|
||||||
|
setCommonBlockPropsFromElement(element, node);
|
||||||
|
|
||||||
|
const colWidths = getTableColumnWidths(element as HTMLTableElement);
|
||||||
|
node.setColWidths(colWidths);
|
||||||
|
node.setStyles(extractStyleMapFromElement(element));
|
||||||
|
|
||||||
|
return {node};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createTableNode(): TableNode {
|
export function $createTableNode(): TableNode {
|
||||||
|
|
|
@ -20,11 +20,12 @@ import {
|
||||||
SerializedElementNode,
|
SerializedElementNode,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
|
|
||||||
import {PIXEL_VALUE_REG_EXP} from './constants';
|
import {extractStyleMapFromElement, sizeToPixels, StyleMap} from "../../utils/dom";
|
||||||
|
|
||||||
export type SerializedTableRowNode = Spread<
|
export type SerializedTableRowNode = Spread<
|
||||||
{
|
{
|
||||||
height?: number;
|
styles: Record<string, string>,
|
||||||
|
height?: number,
|
||||||
},
|
},
|
||||||
SerializedElementNode
|
SerializedElementNode
|
||||||
>;
|
>;
|
||||||
|
@ -33,13 +34,17 @@ export type SerializedTableRowNode = Spread<
|
||||||
export class TableRowNode extends ElementNode {
|
export class TableRowNode extends ElementNode {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
__height?: number;
|
__height?: number;
|
||||||
|
/** @internal */
|
||||||
|
__styles: StyleMap = new Map();
|
||||||
|
|
||||||
static getType(): string {
|
static getType(): string {
|
||||||
return 'tablerow';
|
return 'tablerow';
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: TableRowNode): TableRowNode {
|
static clone(node: TableRowNode): TableRowNode {
|
||||||
return new TableRowNode(node.__height, node.__key);
|
const newNode = new TableRowNode(node.__key);
|
||||||
|
newNode.__styles = new Map(node.__styles);
|
||||||
|
return newNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap | null {
|
static importDOM(): DOMConversionMap | null {
|
||||||
|
@ -52,20 +57,24 @@ export class TableRowNode extends ElementNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedTableRowNode): TableRowNode {
|
static importJSON(serializedNode: SerializedTableRowNode): TableRowNode {
|
||||||
return $createTableRowNode(serializedNode.height);
|
const node = $createTableRowNode();
|
||||||
|
|
||||||
|
node.setStyles(new Map(Object.entries(serializedNode.styles)));
|
||||||
|
|
||||||
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(height?: number, key?: NodeKey) {
|
constructor(key?: NodeKey) {
|
||||||
super(key);
|
super(key);
|
||||||
this.__height = height;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exportJSON(): SerializedTableRowNode {
|
exportJSON(): SerializedTableRowNode {
|
||||||
return {
|
return {
|
||||||
...super.exportJSON(),
|
...super.exportJSON(),
|
||||||
...(this.getHeight() && {height: this.getHeight()}),
|
|
||||||
type: 'tablerow',
|
type: 'tablerow',
|
||||||
version: 1,
|
version: 1,
|
||||||
|
styles: Object.fromEntries(this.__styles),
|
||||||
|
height: this.__height || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +85,10 @@ export class TableRowNode extends ElementNode {
|
||||||
element.style.height = `${this.__height}px`;
|
element.style.height = `${this.__height}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [name, value] of this.__styles.entries()) {
|
||||||
|
element.style.setProperty(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
addClassNamesToElement(element, config.theme.tableRow);
|
addClassNamesToElement(element, config.theme.tableRow);
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
|
@ -85,6 +98,16 @@ export class TableRowNode extends ElementNode {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStyles(): StyleMap {
|
||||||
|
const self = this.getLatest();
|
||||||
|
return new Map(self.__styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStyles(styles: StyleMap): void {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__styles = new Map(styles);
|
||||||
|
}
|
||||||
|
|
||||||
setHeight(height: number): number | null | undefined {
|
setHeight(height: number): number | null | undefined {
|
||||||
const self = this.getWritable();
|
const self = this.getWritable();
|
||||||
self.__height = height;
|
self.__height = height;
|
||||||
|
@ -96,7 +119,8 @@ export class TableRowNode extends ElementNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDOM(prevNode: TableRowNode): boolean {
|
updateDOM(prevNode: TableRowNode): boolean {
|
||||||
return prevNode.__height !== this.__height;
|
return prevNode.__height !== this.__height
|
||||||
|
|| prevNode.__styles !== this.__styles;
|
||||||
}
|
}
|
||||||
|
|
||||||
canBeEmpty(): false {
|
canBeEmpty(): false {
|
||||||
|
@ -109,18 +133,21 @@ export class TableRowNode extends ElementNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $convertTableRowElement(domNode: Node): DOMConversionOutput {
|
export function $convertTableRowElement(domNode: Node): DOMConversionOutput {
|
||||||
const domNode_ = domNode as HTMLTableCellElement;
|
const rowNode = $createTableRowNode();
|
||||||
let height: number | undefined = undefined;
|
const domNode_ = domNode as HTMLElement;
|
||||||
|
|
||||||
if (PIXEL_VALUE_REG_EXP.test(domNode_.style.height)) {
|
const height = sizeToPixels(domNode_.style.height);
|
||||||
height = parseFloat(domNode_.style.height);
|
rowNode.setHeight(height);
|
||||||
|
|
||||||
|
if (domNode instanceof HTMLElement) {
|
||||||
|
rowNode.setStyles(extractStyleMapFromElement(domNode));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {node: $createTableRowNode(height)};
|
return {node: rowNode};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createTableRowNode(height?: number): TableRowNode {
|
export function $createTableRowNode(): TableRowNode {
|
||||||
return $applyNodeReplacement(new TableRowNode(height));
|
return $applyNodeReplacement(new TableRowNode());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $isTableRowNode(
|
export function $isTableRowNode(
|
||||||
|
|
|
@ -16,7 +16,6 @@ import type {
|
||||||
} from './LexicalTableSelection';
|
} from './LexicalTableSelection';
|
||||||
import type {
|
import type {
|
||||||
BaseSelection,
|
BaseSelection,
|
||||||
ElementFormatType,
|
|
||||||
LexicalCommand,
|
LexicalCommand,
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
LexicalNode,
|
LexicalNode,
|
||||||
|
@ -50,7 +49,6 @@ import {
|
||||||
DELETE_LINE_COMMAND,
|
DELETE_LINE_COMMAND,
|
||||||
DELETE_WORD_COMMAND,
|
DELETE_WORD_COMMAND,
|
||||||
FOCUS_COMMAND,
|
FOCUS_COMMAND,
|
||||||
FORMAT_ELEMENT_COMMAND,
|
|
||||||
FORMAT_TEXT_COMMAND,
|
FORMAT_TEXT_COMMAND,
|
||||||
INSERT_PARAGRAPH_COMMAND,
|
INSERT_PARAGRAPH_COMMAND,
|
||||||
KEY_ARROW_DOWN_COMMAND,
|
KEY_ARROW_DOWN_COMMAND,
|
||||||
|
@ -438,59 +436,6 @@ export function applyTableHandlers(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
tableObserver.listenersToRemove.add(
|
|
||||||
editor.registerCommand<ElementFormatType>(
|
|
||||||
FORMAT_ELEMENT_COMMAND,
|
|
||||||
(formatType) => {
|
|
||||||
const selection = $getSelection();
|
|
||||||
if (
|
|
||||||
!$isTableSelection(selection) ||
|
|
||||||
!$isSelectionInTable(selection, tableNode)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const anchorNode = selection.anchor.getNode();
|
|
||||||
const focusNode = selection.focus.getNode();
|
|
||||||
if (!$isTableCellNode(anchorNode) || !$isTableCellNode(focusNode)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [tableMap, anchorCell, focusCell] = $computeTableMap(
|
|
||||||
tableNode,
|
|
||||||
anchorNode,
|
|
||||||
focusNode,
|
|
||||||
);
|
|
||||||
const maxRow = Math.max(anchorCell.startRow, focusCell.startRow);
|
|
||||||
const maxColumn = Math.max(
|
|
||||||
anchorCell.startColumn,
|
|
||||||
focusCell.startColumn,
|
|
||||||
);
|
|
||||||
const minRow = Math.min(anchorCell.startRow, focusCell.startRow);
|
|
||||||
const minColumn = Math.min(
|
|
||||||
anchorCell.startColumn,
|
|
||||||
focusCell.startColumn,
|
|
||||||
);
|
|
||||||
for (let i = minRow; i <= maxRow; i++) {
|
|
||||||
for (let j = minColumn; j <= maxColumn; j++) {
|
|
||||||
const cell = tableMap[i][j].cell;
|
|
||||||
cell.setFormat(formatType);
|
|
||||||
|
|
||||||
const cellChildren = cell.getChildren();
|
|
||||||
for (let k = 0; k < cellChildren.length; k++) {
|
|
||||||
const child = cellChildren[k];
|
|
||||||
if ($isElementNode(child) && !child.isInline()) {
|
|
||||||
child.setFormat(formatType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
COMMAND_PRIORITY_CRITICAL,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
tableObserver.listenersToRemove.add(
|
tableObserver.listenersToRemove.add(
|
||||||
editor.registerCommand(
|
editor.registerCommand(
|
||||||
CONTROLLED_TEXT_INSERTION_COMMAND,
|
CONTROLLED_TEXT_INSERTION_COMMAND,
|
||||||
|
|
|
@ -113,9 +113,8 @@ describe('LexicalTableNode tests', () => {
|
||||||
$insertDataTransferForRichText(dataTransfer, selection, editor);
|
$insertDataTransferForRichText(dataTransfer, selection, editor);
|
||||||
});
|
});
|
||||||
// Make sure paragraph is inserted inside empty cells
|
// Make sure paragraph is inserted inside empty cells
|
||||||
const emptyCell = '<td><p><br></p></td>';
|
|
||||||
expect(testEnv.innerHTML).toBe(
|
expect(testEnv.innerHTML).toBe(
|
||||||
`<table><tr><td><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Hello there</span></p></td><td><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">General Kenobi!</span></p></td></tr><tr><td><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Lexical is nice</span></p></td>${emptyCell}</tr></table>`,
|
`<table style="border-collapse: collapse; table-layout: fixed; width: 468pt;"><colgroup><col><col></colgroup><tr style="height: 22.015pt;"><td style="border-left: 1pt solid #000000; border-right: 1pt solid #000000; border-bottom: 1pt solid #000000; border-top: 1pt solid #000000; vertical-align: top; padding: 5pt 5pt 5pt 5pt; overflow: hidden; overflow-wrap: break-word;"><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Hello there</span></p></td><td style="border-left: 1pt solid #000000; border-right: 1pt solid #000000; border-bottom: 1pt solid #000000; border-top: 1pt solid #000000; vertical-align: top; padding: 5pt 5pt 5pt 5pt; overflow: hidden; overflow-wrap: break-word;"><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">General Kenobi!</span></p></td></tr><tr style="height: 22.015pt;"><td style="border-left: 1pt solid #000000; border-right: 1pt solid #000000; border-bottom: 1pt solid #000000; border-top: 1pt solid #000000; vertical-align: top; padding: 5pt 5pt 5pt 5pt; overflow: hidden; overflow-wrap: break-word;"><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Lexical is nice</span></p></td><td style="border-left: 1pt solid #000000; border-right: 1pt solid #000000; border-bottom: 1pt solid #000000; border-top: 1pt solid #000000; vertical-align: top; padding: 5pt 5pt 5pt 5pt; overflow: hidden; overflow-wrap: break-word;"><p><br></p></td></tr></table>`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -136,7 +135,7 @@ describe('LexicalTableNode tests', () => {
|
||||||
$insertDataTransferForRichText(dataTransfer, selection, editor);
|
$insertDataTransferForRichText(dataTransfer, selection, editor);
|
||||||
});
|
});
|
||||||
expect(testEnv.innerHTML).toBe(
|
expect(testEnv.innerHTML).toBe(
|
||||||
`<table><tr style="height: 21px;"><td><p><strong data-lexical-text="true">Surface</strong></p></td><td><p><em data-lexical-text="true">MWP_WORK_LS_COMPOSER</em></p></td><td><p style="text-align: right;"><span data-lexical-text="true">77349</span></p></td></tr><tr style="height: 21px;"><td><p><span data-lexical-text="true">Lexical</span></p></td><td><p><span data-lexical-text="true">XDS_RICH_TEXT_AREA</span></p></td><td><p><span data-lexical-text="true">sdvd </span><strong data-lexical-text="true">sdfvsfs</strong></p></td></tr></table>`,
|
`<table style="table-layout: fixed; font-size: 10pt; font-family: Arial; width: 0px; border-collapse: collapse;"><colgroup><col style="width: 100px;"><col style="width: 189px;"><col style="width: 171px;"></colgroup><tr style="height: 21px;"><td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom; font-weight: bold;"><p><strong data-lexical-text="true">Surface</strong></p></td><td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom; font-style: italic;"><p><em data-lexical-text="true">MWP_WORK_LS_COMPOSER</em></p></td><td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom; text-decoration: underline; text-align: right;" class="align-right"><p><span data-lexical-text="true">77349</span></p></td></tr><tr style="height: 21px;"><td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;"><p><span data-lexical-text="true">Lexical</span></p></td><td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom; text-decoration: line-through;"><p><span data-lexical-text="true">XDS_RICH_TEXT_AREA</span></p></td><td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;"><p><span data-lexical-text="true">sdvd </span><strong data-lexical-text="true">sdfvsfs</strong></p></td></tr></table>`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -39,10 +39,9 @@ describe('LexicalTableRowNode tests', () => {
|
||||||
`<tr class="${editorConfig.theme.tableRow}"></tr>`,
|
`<tr class="${editorConfig.theme.tableRow}"></tr>`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const rowHeight = 36;
|
const rowWithCustomHeightNode = $createTableRowNode();
|
||||||
const rowWithCustomHeightNode = $createTableRowNode(36);
|
|
||||||
expect(rowWithCustomHeightNode.createDOM(editorConfig).outerHTML).toBe(
|
expect(rowWithCustomHeightNode.createDOM(editorConfig).outerHTML).toBe(
|
||||||
`<tr style="height: ${rowHeight}px;" class="${editorConfig.theme.tableRow}"></tr>`,
|
`<tr class="${editorConfig.theme.tableRow}"></tr>`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -101,8 +101,6 @@ describe('table selection', () => {
|
||||||
__cachedText: null,
|
__cachedText: null,
|
||||||
__dir: null,
|
__dir: null,
|
||||||
__first: paragraphKey,
|
__first: paragraphKey,
|
||||||
__format: 0,
|
|
||||||
__indent: 0,
|
|
||||||
__key: 'root',
|
__key: 'root',
|
||||||
__last: paragraphKey,
|
__last: paragraphKey,
|
||||||
__next: null,
|
__next: null,
|
||||||
|
@ -113,10 +111,11 @@ describe('table selection', () => {
|
||||||
__type: 'root',
|
__type: 'root',
|
||||||
});
|
});
|
||||||
expect(parsedParagraph).toEqual({
|
expect(parsedParagraph).toEqual({
|
||||||
|
__alignment: "",
|
||||||
__dir: null,
|
__dir: null,
|
||||||
__first: textKey,
|
__first: textKey,
|
||||||
__format: 0,
|
__id: '',
|
||||||
__indent: 0,
|
__inset: 0,
|
||||||
__key: paragraphKey,
|
__key: paragraphKey,
|
||||||
__last: textKey,
|
__last: textKey,
|
||||||
__next: null,
|
__next: null,
|
||||||
|
@ -124,7 +123,6 @@ describe('table selection', () => {
|
||||||
__prev: null,
|
__prev: null,
|
||||||
__size: 1,
|
__size: 1,
|
||||||
__style: '',
|
__style: '',
|
||||||
__textFormat: 0,
|
|
||||||
__textStyle: '',
|
__textStyle: '',
|
||||||
__type: 'paragraph',
|
__type: 'paragraph',
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
import {AutoLinkNode, LinkNode} from '@lexical/link';
|
import {AutoLinkNode, LinkNode} from '@lexical/link';
|
||||||
import {ListItemNode, ListNode} from '@lexical/list';
|
import {ListItemNode, ListNode} from '@lexical/list';
|
||||||
import {HeadingNode, QuoteNode, registerRichText} from '@lexical/rich-text';
|
import {registerRichText} from '@lexical/rich-text';
|
||||||
import {
|
import {
|
||||||
applySelectionInputs,
|
applySelectionInputs,
|
||||||
pasteHTML,
|
pasteHTML,
|
||||||
|
@ -15,6 +15,8 @@ import {
|
||||||
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
|
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
|
||||||
import {$createParagraphNode, $insertNodes, LexicalEditor} from 'lexical';
|
import {$createParagraphNode, $insertNodes, LexicalEditor} from 'lexical';
|
||||||
import {createTestEditor, initializeClipboard} from 'lexical/__tests__/utils';
|
import {createTestEditor, initializeClipboard} from 'lexical/__tests__/utils';
|
||||||
|
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
|
||||||
|
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
|
||||||
|
|
||||||
jest.mock('lexical/shared/environment', () => {
|
jest.mock('lexical/shared/environment', () => {
|
||||||
const originalModule = jest.requireActual('lexical/shared/environment');
|
const originalModule = jest.requireActual('lexical/shared/environment');
|
||||||
|
@ -174,7 +176,7 @@ describe('LexicalEventHelpers', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectedHTML:
|
expectedHTML:
|
||||||
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1" class="editor-listitem"><span data-lexical-text="true">Other side</span></li><li value="2" class="editor-listitem"><span data-lexical-text="true">I must have called</span></li></ul></div>',
|
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1"><span data-lexical-text="true">Other side</span></li><li value="2"><span data-lexical-text="true">I must have called</span></li></ul></div>',
|
||||||
inputs: [
|
inputs: [
|
||||||
pasteHTML(
|
pasteHTML(
|
||||||
`<meta charset='utf-8'><ul><li>Other side</li><li>I must have called</li></ul>`,
|
`<meta charset='utf-8'><ul><li>Other side</li><li>I must have called</li></ul>`,
|
||||||
|
@ -184,7 +186,7 @@ describe('LexicalEventHelpers', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectedHTML:
|
expectedHTML:
|
||||||
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ol class="editor-list-ol"><li value="1" class="editor-listitem"><span data-lexical-text="true">To tell you</span></li><li value="2" class="editor-listitem"><span data-lexical-text="true">I’m 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">I’m sorry</span></li></ol></div>',
|
||||||
inputs: [
|
inputs: [
|
||||||
pasteHTML(
|
pasteHTML(
|
||||||
`<meta charset='utf-8'><ol><li>To tell you</li><li>I’m sorry</li></ol>`,
|
`<meta charset='utf-8'><ol><li>To tell you</li><li>I’m sorry</li></ol>`,
|
||||||
|
@ -264,7 +266,7 @@ describe('LexicalEventHelpers', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectedHTML:
|
expectedHTML:
|
||||||
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1" class="editor-listitem"><span data-lexical-text="true">Hello</span></li><li value="2" class="editor-listitem"><span data-lexical-text="true">from the other</span></li><li value="3" class="editor-listitem"><span data-lexical-text="true">side</span></li></ul></div>',
|
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1"><span data-lexical-text="true">Hello</span></li><li value="2"><span data-lexical-text="true">from the other</span></li><li value="3"><span data-lexical-text="true">side</span></li></ul></div>',
|
||||||
inputs: [
|
inputs: [
|
||||||
pasteHTML(
|
pasteHTML(
|
||||||
`<meta charset='utf-8'><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist>`,
|
`<meta charset='utf-8'><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist>`,
|
||||||
|
@ -274,7 +276,7 @@ describe('LexicalEventHelpers', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectedHTML:
|
expectedHTML:
|
||||||
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1" class="editor-listitem"><span data-lexical-text="true">Hello</span></li><li value="2" class="editor-listitem"><span data-lexical-text="true">from the other</span></li><li value="3" class="editor-listitem"><span data-lexical-text="true">side</span></li></ul></div>',
|
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1"><span data-lexical-text="true">Hello</span></li><li value="2"><span data-lexical-text="true">from the other</span></li><li value="3"><span data-lexical-text="true">side</span></li></ul></div>',
|
||||||
inputs: [
|
inputs: [
|
||||||
pasteHTML(
|
pasteHTML(
|
||||||
`<meta charset='utf-8'><doesnotexist><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist></doesnotexist>`,
|
`<meta charset='utf-8'><doesnotexist><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist></doesnotexist>`,
|
||||||
|
@ -609,7 +611,7 @@ describe('LexicalEventHelpers', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectedHTML:
|
expectedHTML:
|
||||||
'<ol class="editor-list-ol"><li value="1" class="editor-listitem"><span data-lexical-text="true">1</span><br><span data-lexical-text="true">2</span></li><li value="2" class="editor-listitem"><br></li><li value="3" class="editor-listitem"><span data-lexical-text="true">3</span></li></ol>',
|
'<ol class="editor-list-ol"><li value="1"><span data-lexical-text="true">1</span><br><span data-lexical-text="true">2</span></li><li value="2"><br></li><li value="3"><span data-lexical-text="true">3</span></li></ol>',
|
||||||
inputs: [
|
inputs: [
|
||||||
pasteHTML('<ol><li>1<div></div>2</li><li></li><li>3</li></ol>'),
|
pasteHTML('<ol><li>1<div></div>2</li><li></li><li>3</li></ol>'),
|
||||||
],
|
],
|
||||||
|
@ -645,7 +647,7 @@ describe('LexicalEventHelpers', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectedHTML:
|
expectedHTML:
|
||||||
'<ol class="editor-list-ol"><li value="1" class="editor-listitem"><span data-lexical-text="true">1</span></li><li value="2" class="editor-listitem"><br></li><li value="3" class="editor-listitem"><span data-lexical-text="true">3</span></li></ol>',
|
'<ol class="editor-list-ol"><li value="1"><span data-lexical-text="true">1</span></li><li value="2"><br></li><li value="3"><span data-lexical-text="true">3</span></li></ol>',
|
||||||
inputs: [pasteHTML('<ol><li>1</li><li><br /></li><li>3</li></ol>')],
|
inputs: [pasteHTML('<ol><li>1</li><li><br /></li><li>3</li></ol>')],
|
||||||
name: 'only br in a li',
|
name: 'only br in a li',
|
||||||
},
|
},
|
||||||
|
|
|
@ -82,10 +82,10 @@ describe('LexicalUtils#splitNode', () => {
|
||||||
expectedHtml:
|
expectedHtml:
|
||||||
'<ul>' +
|
'<ul>' +
|
||||||
'<li>Before</li>' +
|
'<li>Before</li>' +
|
||||||
'<li><ul><li>Hello</li></ul></li>' +
|
'<li style="list-style: none;"><ul><li>Hello</li></ul></li>' +
|
||||||
'</ul>' +
|
'</ul>' +
|
||||||
'<ul>' +
|
'<ul>' +
|
||||||
'<li><ul><li>world</li></ul></li>' +
|
'<li style="list-style: none;"><ul><li>world</li></ul></li>' +
|
||||||
'<li>After</li>' +
|
'<li>After</li>' +
|
||||||
'</ul>',
|
'</ul>',
|
||||||
initialHtml:
|
initialHtml:
|
||||||
|
|
|
@ -56,11 +56,11 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
|
||||||
expectedHtml:
|
expectedHtml:
|
||||||
'<ul>' +
|
'<ul>' +
|
||||||
'<li>Before</li>' +
|
'<li>Before</li>' +
|
||||||
'<li><ul><li>Hello</li></ul></li>' +
|
'<li style="list-style: none;"><ul><li>Hello</li></ul></li>' +
|
||||||
'</ul>' +
|
'</ul>' +
|
||||||
'<test-decorator></test-decorator>' +
|
'<test-decorator></test-decorator>' +
|
||||||
'<ul>' +
|
'<ul>' +
|
||||||
'<li><ul><li>world</li></ul></li>' +
|
'<li style="list-style: none;"><ul><li>world</li></ul></li>' +
|
||||||
'<li>After</li>' +
|
'<li>After</li>' +
|
||||||
'</ul>',
|
'</ul>',
|
||||||
initialHtml:
|
initialHtml:
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
$createParagraphNode,
|
||||||
$insertNodes,
|
$insertNodes,
|
||||||
$isDecoratorNode, COMMAND_PRIORITY_HIGH, DROP_COMMAND,
|
$isDecoratorNode, COMMAND_PRIORITY_HIGH, DROP_COMMAND,
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
|
@ -7,8 +8,7 @@ import {
|
||||||
import {$insertNewBlockNodesAtSelection, $selectSingleNode} from "../utils/selection";
|
import {$insertNewBlockNodesAtSelection, $selectSingleNode} from "../utils/selection";
|
||||||
import {$getNearestBlockNodeForCoords, $htmlToBlockNodes} from "../utils/nodes";
|
import {$getNearestBlockNodeForCoords, $htmlToBlockNodes} from "../utils/nodes";
|
||||||
import {Clipboard} from "../../services/clipboard";
|
import {Clipboard} from "../../services/clipboard";
|
||||||
import {$createImageNode} from "../nodes/image";
|
import {$createImageNode} from "@lexical/rich-text/LexicalImageNode";
|
||||||
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
|
|
||||||
import {$createLinkNode} from "@lexical/link";
|
import {$createLinkNode} from "@lexical/link";
|
||||||
import {EditorImageData, uploadImageFile} from "../utils/images";
|
import {EditorImageData, uploadImageFile} from "../utils/images";
|
||||||
import {EditorUiContext} from "../ui/framework/core";
|
import {EditorUiContext} from "../ui/framework/core";
|
||||||
|
@ -67,7 +67,7 @@ function handleMediaInsert(data: DataTransfer, context: EditorUiContext): boolea
|
||||||
for (const imageFile of images) {
|
for (const imageFile of images) {
|
||||||
const loadingImage = window.baseUrl('/loading.gif');
|
const loadingImage = window.baseUrl('/loading.gif');
|
||||||
const loadingNode = $createImageNode(loadingImage);
|
const loadingNode = $createImageNode(loadingImage);
|
||||||
const imageWrap = $createCustomParagraphNode();
|
const imageWrap = $createParagraphNode();
|
||||||
imageWrap.append(loadingNode);
|
imageWrap.append(loadingNode);
|
||||||
$insertNodes([imageWrap]);
|
$insertNodes([imageWrap]);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {EditorUiContext} from "../ui/framework/core";
|
import {EditorUiContext} from "../ui/framework/core";
|
||||||
import {
|
import {
|
||||||
|
$createParagraphNode,
|
||||||
$getSelection,
|
$getSelection,
|
||||||
$isDecoratorNode,
|
$isDecoratorNode,
|
||||||
COMMAND_PRIORITY_LOW,
|
COMMAND_PRIORITY_LOW,
|
||||||
|
@ -9,13 +10,12 @@ import {
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
LexicalNode
|
LexicalNode
|
||||||
} from "lexical";
|
} from "lexical";
|
||||||
import {$isImageNode} from "../nodes/image";
|
import {$isImageNode} from "@lexical/rich-text/LexicalImageNode";
|
||||||
import {$isMediaNode} from "../nodes/media";
|
import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode";
|
||||||
import {getLastSelection} from "../utils/selection";
|
import {getLastSelection} from "../utils/selection";
|
||||||
import {$getNearestNodeBlockParent} from "../utils/nodes";
|
import {$getNearestNodeBlockParent} from "../utils/nodes";
|
||||||
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
|
|
||||||
import {$isCustomListItemNode} from "../nodes/custom-list-item";
|
|
||||||
import {$setInsetForSelection} from "../utils/lists";
|
import {$setInsetForSelection} from "../utils/lists";
|
||||||
|
import {$isListItemNode} from "@lexical/list";
|
||||||
|
|
||||||
function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
|
function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
|
||||||
if (nodes.length === 1) {
|
if (nodes.length === 1) {
|
||||||
|
@ -45,7 +45,7 @@ function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEve
|
||||||
if (nearestBlock) {
|
if (nearestBlock) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const newParagraph = $createCustomParagraphNode();
|
const newParagraph = $createParagraphNode();
|
||||||
nearestBlock.insertAfter(newParagraph);
|
nearestBlock.insertAfter(newParagraph);
|
||||||
newParagraph.select();
|
newParagraph.select();
|
||||||
});
|
});
|
||||||
|
@ -62,7 +62,7 @@ function handleInsetOnTab(editor: LexicalEditor, event: KeyboardEvent|null): boo
|
||||||
const change = event?.shiftKey ? -40 : 40;
|
const change = event?.shiftKey ? -40 : 40;
|
||||||
const selection = $getSelection();
|
const selection = $getSelection();
|
||||||
const nodes = selection?.getNodes() || [];
|
const nodes = selection?.getNodes() || [];
|
||||||
if (nodes.length > 1 || (nodes.length === 1 && $isCustomListItemNode(nodes[0].getParent()))) {
|
if (nodes.length > 1 || (nodes.length === 1 && $isListItemNode(nodes[0].getParent()))) {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
$setInsetForSelection(editor, change);
|
$setInsetForSelection(editor, change);
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,12 +6,12 @@ import {
|
||||||
toggleSelectionAsHeading, toggleSelectionAsList,
|
toggleSelectionAsHeading, toggleSelectionAsList,
|
||||||
toggleSelectionAsParagraph
|
toggleSelectionAsParagraph
|
||||||
} from "../utils/formats";
|
} from "../utils/formats";
|
||||||
import {HeadingTagType} from "@lexical/rich-text";
|
|
||||||
import {EditorUiContext} from "../ui/framework/core";
|
import {EditorUiContext} from "../ui/framework/core";
|
||||||
import {$getNodeFromSelection} from "../utils/selection";
|
import {$getNodeFromSelection} from "../utils/selection";
|
||||||
import {$isLinkNode, LinkNode} from "@lexical/link";
|
import {$isLinkNode, LinkNode} from "@lexical/link";
|
||||||
import {$showLinkForm} from "../ui/defaults/forms/objects";
|
import {$showLinkForm} from "../ui/defaults/forms/objects";
|
||||||
import {showLinkSelector} from "../utils/links";
|
import {showLinkSelector} from "../utils/links";
|
||||||
|
import {HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode";
|
||||||
|
|
||||||
function headerHandler(editor: LexicalEditor, tag: HeadingTagType): boolean {
|
function headerHandler(editor: LexicalEditor, tag: HeadingTagType): boolean {
|
||||||
toggleSelectionAsHeading(editor, tag);
|
toggleSelectionAsHeading(editor, tag);
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
|
|
||||||
## In progress
|
## In progress
|
||||||
|
|
||||||
//
|
Reorg
|
||||||
|
- Merge custom nodes into original nodes
|
||||||
|
- Reduce down to use CommonBlockNode where possible
|
||||||
|
- Remove existing formatType/ElementFormatType references (replaced with alignment).
|
||||||
|
- Remove existing indent references (replaced with inset).
|
||||||
|
|
||||||
## Main Todo
|
## Main Todo
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {EditorDecorator} from "../framework/decorator";
|
import {EditorDecorator} from "../framework/decorator";
|
||||||
import {EditorUiContext} from "../framework/core";
|
import {EditorUiContext} from "../framework/core";
|
||||||
import {$openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
|
import {$openCodeEditorForNode, CodeBlockNode} from "@lexical/rich-text/LexicalCodeBlockNode";
|
||||||
import {$isDecoratorNode, BaseSelection} from "lexical";
|
import {BaseSelection} from "lexical";
|
||||||
import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection";
|
import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection";
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {EditorDecorator} from "../framework/decorator";
|
import {EditorDecorator} from "../framework/decorator";
|
||||||
import {EditorUiContext} from "../framework/core";
|
import {EditorUiContext} from "../framework/core";
|
||||||
import {BaseSelection} from "lexical";
|
import {BaseSelection} from "lexical";
|
||||||
import {DiagramNode} from "../../nodes/diagram";
|
import {DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
|
||||||
import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection";
|
import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection";
|
||||||
import {$openDrawingEditorForNode} from "../../utils/diagrams";
|
import {$openDrawingEditorForNode} from "../../utils/diagrams";
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,9 @@ import ltrIcon from "@icons/editor/direction-ltr.svg";
|
||||||
import rtlIcon from "@icons/editor/direction-rtl.svg";
|
import rtlIcon from "@icons/editor/direction-rtl.svg";
|
||||||
import {
|
import {
|
||||||
$getBlockElementNodesInSelection,
|
$getBlockElementNodesInSelection,
|
||||||
$selectionContainsAlignment, $selectionContainsDirection, $selectSingleNode, $toggleSelection, getLastSelection
|
$selectionContainsAlignment, $selectionContainsDirection, $selectSingleNode, getLastSelection
|
||||||
} from "../../../utils/selection";
|
} from "../../../utils/selection";
|
||||||
import {CommonBlockAlignment} from "../../../nodes/_common";
|
import {CommonBlockAlignment} from "lexical/nodes/common";
|
||||||
import {nodeHasAlignment} from "../../../utils/nodes";
|
import {nodeHasAlignment} from "../../../utils/nodes";
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../../nodes/callout";
|
import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "@lexical/rich-text/LexicalCalloutNode";
|
||||||
import {EditorButtonDefinition} from "../../framework/buttons";
|
import {EditorButtonDefinition} from "../../framework/buttons";
|
||||||
import {EditorUiContext} from "../../framework/core";
|
import {EditorUiContext} from "../../framework/core";
|
||||||
import {$isParagraphNode, BaseSelection, LexicalNode} from "lexical";
|
import {$isParagraphNode, BaseSelection, LexicalNode} from "lexical";
|
||||||
import {
|
|
||||||
$isHeadingNode,
|
|
||||||
$isQuoteNode,
|
|
||||||
HeadingNode,
|
|
||||||
HeadingTagType
|
|
||||||
} from "@lexical/rich-text";
|
|
||||||
import {$selectionContainsNodeType, $toggleSelectionBlockNodeType} from "../../../utils/selection";
|
import {$selectionContainsNodeType, $toggleSelectionBlockNodeType} from "../../../utils/selection";
|
||||||
import {
|
import {
|
||||||
toggleSelectionAsBlockquote,
|
toggleSelectionAsBlockquote,
|
||||||
toggleSelectionAsHeading,
|
toggleSelectionAsHeading,
|
||||||
toggleSelectionAsParagraph
|
toggleSelectionAsParagraph
|
||||||
} from "../../../utils/formats";
|
} from "../../../utils/formats";
|
||||||
|
import {$isHeadingNode, HeadingNode, HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode";
|
||||||
|
import {$isQuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
|
||||||
|
|
||||||
function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
|
function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -2,27 +2,26 @@ import {EditorButtonDefinition} from "../../framework/buttons";
|
||||||
import linkIcon from "@icons/editor/link.svg";
|
import linkIcon from "@icons/editor/link.svg";
|
||||||
import {EditorUiContext} from "../../framework/core";
|
import {EditorUiContext} from "../../framework/core";
|
||||||
import {
|
import {
|
||||||
$createTextNode,
|
|
||||||
$getRoot,
|
$getRoot,
|
||||||
$getSelection, $insertNodes,
|
$getSelection, $insertNodes,
|
||||||
BaseSelection,
|
BaseSelection,
|
||||||
ElementNode, isCurrentlyReadOnlyMode
|
ElementNode
|
||||||
} from "lexical";
|
} from "lexical";
|
||||||
import {$isLinkNode, LinkNode} from "@lexical/link";
|
import {$isLinkNode, LinkNode} from "@lexical/link";
|
||||||
import unlinkIcon from "@icons/editor/unlink.svg";
|
import unlinkIcon from "@icons/editor/unlink.svg";
|
||||||
import imageIcon from "@icons/editor/image.svg";
|
import imageIcon from "@icons/editor/image.svg";
|
||||||
import {$isImageNode, ImageNode} from "../../../nodes/image";
|
import {$isImageNode, ImageNode} from "@lexical/rich-text/LexicalImageNode";
|
||||||
import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg";
|
import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg";
|
||||||
import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../../nodes/horizontal-rule";
|
import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "@lexical/rich-text/LexicalHorizontalRuleNode";
|
||||||
import codeBlockIcon from "@icons/editor/code-block.svg";
|
import codeBlockIcon from "@icons/editor/code-block.svg";
|
||||||
import {$isCodeBlockNode} from "../../../nodes/code-block";
|
import {$isCodeBlockNode} from "@lexical/rich-text/LexicalCodeBlockNode";
|
||||||
import editIcon from "@icons/edit.svg";
|
import editIcon from "@icons/edit.svg";
|
||||||
import diagramIcon from "@icons/editor/diagram.svg";
|
import diagramIcon from "@icons/editor/diagram.svg";
|
||||||
import {$createDiagramNode, DiagramNode} from "../../../nodes/diagram";
|
import {$createDiagramNode, DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
|
||||||
import detailsIcon from "@icons/editor/details.svg";
|
import detailsIcon from "@icons/editor/details.svg";
|
||||||
import mediaIcon from "@icons/editor/media.svg";
|
import mediaIcon from "@icons/editor/media.svg";
|
||||||
import {$createDetailsNode, $isDetailsNode} from "../../../nodes/details";
|
import {$createDetailsNode, $isDetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
|
||||||
import {$isMediaNode, MediaNode} from "../../../nodes/media";
|
import {$isMediaNode, MediaNode} from "@lexical/rich-text/LexicalMediaNode";
|
||||||
import {
|
import {
|
||||||
$getNodeFromSelection,
|
$getNodeFromSelection,
|
||||||
$insertNewBlockNodeAtSelection,
|
$insertNewBlockNodeAtSelection,
|
||||||
|
|
|
@ -9,17 +9,15 @@ import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg";
|
||||||
import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
|
import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
|
||||||
import {EditorUiContext} from "../../framework/core";
|
import {EditorUiContext} from "../../framework/core";
|
||||||
import {$getSelection, BaseSelection} from "lexical";
|
import {$getSelection, BaseSelection} from "lexical";
|
||||||
import {$isCustomTableNode} from "../../../nodes/custom-table";
|
|
||||||
import {
|
import {
|
||||||
$deleteTableColumn__EXPERIMENTAL,
|
$deleteTableColumn__EXPERIMENTAL,
|
||||||
$deleteTableRow__EXPERIMENTAL,
|
$deleteTableRow__EXPERIMENTAL,
|
||||||
$insertTableColumn__EXPERIMENTAL,
|
$insertTableColumn__EXPERIMENTAL,
|
||||||
$insertTableRow__EXPERIMENTAL,
|
$insertTableRow__EXPERIMENTAL, $isTableCellNode,
|
||||||
$isTableNode, $isTableSelection, $unmergeCell, TableCellNode,
|
$isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode,
|
||||||
} from "@lexical/table";
|
} from "@lexical/table";
|
||||||
import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/selection";
|
import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/selection";
|
||||||
import {$getParentOfType} from "../../../utils/nodes";
|
import {$getParentOfType} from "../../../utils/nodes";
|
||||||
import {$isCustomTableCellNode} from "../../../nodes/custom-table-cell";
|
|
||||||
import {$showCellPropertiesForm, $showRowPropertiesForm, $showTablePropertiesForm} from "../forms/tables";
|
import {$showCellPropertiesForm, $showRowPropertiesForm, $showTablePropertiesForm} from "../forms/tables";
|
||||||
import {
|
import {
|
||||||
$clearTableFormatting,
|
$clearTableFormatting,
|
||||||
|
@ -27,7 +25,6 @@ import {
|
||||||
$getTableRowsFromSelection,
|
$getTableRowsFromSelection,
|
||||||
$mergeTableCellsInSelection
|
$mergeTableCellsInSelection
|
||||||
} from "../../../utils/tables";
|
} from "../../../utils/tables";
|
||||||
import {$isCustomTableRowNode} from "../../../nodes/custom-table-row";
|
|
||||||
import {
|
import {
|
||||||
$copySelectedColumnsToClipboard,
|
$copySelectedColumnsToClipboard,
|
||||||
$copySelectedRowsToClipboard,
|
$copySelectedRowsToClipboard,
|
||||||
|
@ -41,7 +38,7 @@ import {
|
||||||
} from "../../../utils/table-copy-paste";
|
} from "../../../utils/table-copy-paste";
|
||||||
|
|
||||||
const neverActive = (): boolean => false;
|
const neverActive = (): boolean => false;
|
||||||
const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode);
|
const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isTableCellNode);
|
||||||
|
|
||||||
export const table: EditorBasicButtonDefinition = {
|
export const table: EditorBasicButtonDefinition = {
|
||||||
label: 'Table',
|
label: 'Table',
|
||||||
|
@ -54,7 +51,7 @@ export const tableProperties: EditorButtonDefinition = {
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.getEditorState().read(() => {
|
context.editor.getEditorState().read(() => {
|
||||||
const table = $getTableFromSelection($getSelection());
|
const table = $getTableFromSelection($getSelection());
|
||||||
if ($isCustomTableNode(table)) {
|
if ($isTableNode(table)) {
|
||||||
$showTablePropertiesForm(table, context);
|
$showTablePropertiesForm(table, context);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -68,13 +65,13 @@ export const clearTableFormatting: EditorButtonDefinition = {
|
||||||
format: 'long',
|
format: 'long',
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.update(() => {
|
context.editor.update(() => {
|
||||||
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
|
const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
|
||||||
if (!$isCustomTableCellNode(cell)) {
|
if (!$isTableCellNode(cell)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = $getParentOfType(cell, $isTableNode);
|
const table = $getParentOfType(cell, $isTableNode);
|
||||||
if ($isCustomTableNode(table)) {
|
if ($isTableNode(table)) {
|
||||||
$clearTableFormatting(table);
|
$clearTableFormatting(table);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -88,13 +85,13 @@ export const resizeTableToContents: EditorButtonDefinition = {
|
||||||
format: 'long',
|
format: 'long',
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.update(() => {
|
context.editor.update(() => {
|
||||||
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
|
const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
|
||||||
if (!$isCustomTableCellNode(cell)) {
|
if (!$isTableCellNode(cell)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = $getParentOfType(cell, $isCustomTableNode);
|
const table = $getParentOfType(cell, $isTableNode);
|
||||||
if ($isCustomTableNode(table)) {
|
if ($isTableNode(table)) {
|
||||||
$clearTableSizes(table);
|
$clearTableSizes(table);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -108,7 +105,7 @@ export const deleteTable: EditorButtonDefinition = {
|
||||||
icon: deleteIcon,
|
icon: deleteIcon,
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.update(() => {
|
context.editor.update(() => {
|
||||||
const table = $getNodeFromSelection($getSelection(), $isCustomTableNode);
|
const table = $getNodeFromSelection($getSelection(), $isTableNode);
|
||||||
if (table) {
|
if (table) {
|
||||||
table.remove();
|
table.remove();
|
||||||
}
|
}
|
||||||
|
@ -169,7 +166,7 @@ export const rowProperties: EditorButtonDefinition = {
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.getEditorState().read(() => {
|
context.editor.getEditorState().read(() => {
|
||||||
const rows = $getTableRowsFromSelection($getSelection());
|
const rows = $getTableRowsFromSelection($getSelection());
|
||||||
if ($isCustomTableRowNode(rows[0])) {
|
if ($isTableRowNode(rows[0])) {
|
||||||
$showRowPropertiesForm(rows[0], context);
|
$showRowPropertiesForm(rows[0], context);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -350,8 +347,8 @@ export const cellProperties: EditorButtonDefinition = {
|
||||||
format: 'long',
|
format: 'long',
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.getEditorState().read(() => {
|
context.editor.getEditorState().read(() => {
|
||||||
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
|
const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
|
||||||
if ($isCustomTableCellNode(cell)) {
|
if ($isTableCellNode(cell)) {
|
||||||
$showCellPropertiesForm(cell, context);
|
$showCellPropertiesForm(cell, context);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -387,7 +384,7 @@ export const splitCell: EditorButtonDefinition = {
|
||||||
},
|
},
|
||||||
isActive: neverActive,
|
isActive: neverActive,
|
||||||
isDisabled(selection) {
|
isDisabled(selection) {
|
||||||
const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as TableCellNode|null;
|
const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode|null;
|
||||||
if (cell) {
|
if (cell) {
|
||||||
const merged = cell.getRowSpan() > 1 || cell.getColSpan() > 1;
|
const merged = cell.getRowSpan() > 1 || cell.getColSpan() > 1;
|
||||||
return !merged;
|
return !merged;
|
||||||
|
|
|
@ -5,11 +5,10 @@ import {
|
||||||
EditorSelectFormFieldDefinition
|
EditorSelectFormFieldDefinition
|
||||||
} from "../../framework/forms";
|
} from "../../framework/forms";
|
||||||
import {EditorUiContext} from "../../framework/core";
|
import {EditorUiContext} from "../../framework/core";
|
||||||
import {$createNodeSelection, $createTextNode, $getSelection, $insertNodes, $setSelection} from "lexical";
|
import {$createNodeSelection, $getSelection, $insertNodes, $setSelection} from "lexical";
|
||||||
import {$isImageNode, ImageNode} from "../../../nodes/image";
|
import {$isImageNode, ImageNode} from "@lexical/rich-text/LexicalImageNode";
|
||||||
import {$createLinkNode, $isLinkNode, LinkNode} from "@lexical/link";
|
import {LinkNode} from "@lexical/link";
|
||||||
import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "../../../nodes/media";
|
import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "@lexical/rich-text/LexicalMediaNode";
|
||||||
import {$insertNodeToNearestRoot} from "@lexical/utils";
|
|
||||||
import {$getNodeFromSelection, getLastSelection} from "../../../utils/selection";
|
import {$getNodeFromSelection, getLastSelection} from "../../../utils/selection";
|
||||||
import {EditorFormModal} from "../../framework/modals";
|
import {EditorFormModal} from "../../framework/modals";
|
||||||
import {EditorActionField} from "../../framework/blocks/action-field";
|
import {EditorActionField} from "../../framework/blocks/action-field";
|
||||||
|
|
|
@ -5,9 +5,8 @@ import {
|
||||||
EditorSelectFormFieldDefinition
|
EditorSelectFormFieldDefinition
|
||||||
} from "../../framework/forms";
|
} from "../../framework/forms";
|
||||||
import {EditorUiContext} from "../../framework/core";
|
import {EditorUiContext} from "../../framework/core";
|
||||||
import {CustomTableCellNode} from "../../../nodes/custom-table-cell";
|
|
||||||
import {EditorFormModal} from "../../framework/modals";
|
import {EditorFormModal} from "../../framework/modals";
|
||||||
import {$getSelection, ElementFormatType} from "lexical";
|
import {$getSelection} from "lexical";
|
||||||
import {
|
import {
|
||||||
$forEachTableCell, $getCellPaddingForTable,
|
$forEachTableCell, $getCellPaddingForTable,
|
||||||
$getTableCellColumnWidth,
|
$getTableCellColumnWidth,
|
||||||
|
@ -16,8 +15,8 @@ import {
|
||||||
$setTableCellColumnWidth
|
$setTableCellColumnWidth
|
||||||
} from "../../../utils/tables";
|
} from "../../../utils/tables";
|
||||||
import {formatSizeValue} from "../../../utils/dom";
|
import {formatSizeValue} from "../../../utils/dom";
|
||||||
import {CustomTableRowNode} from "../../../nodes/custom-table-row";
|
import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
|
||||||
import {CustomTableNode} from "../../../nodes/custom-table";
|
import {CommonBlockAlignment} from "lexical/nodes/common";
|
||||||
|
|
||||||
const borderStyleInput: EditorSelectFormFieldDefinition = {
|
const borderStyleInput: EditorSelectFormFieldDefinition = {
|
||||||
label: 'Border style',
|
label: 'Border style',
|
||||||
|
@ -62,14 +61,14 @@ const alignmentInput: EditorSelectFormFieldDefinition = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function $showCellPropertiesForm(cell: CustomTableCellNode, context: EditorUiContext): EditorFormModal {
|
export function $showCellPropertiesForm(cell: TableCellNode, context: EditorUiContext): EditorFormModal {
|
||||||
const styles = cell.getStyles();
|
const styles = cell.getStyles();
|
||||||
const modalForm = context.manager.createModal('cell_properties');
|
const modalForm = context.manager.createModal('cell_properties');
|
||||||
modalForm.show({
|
modalForm.show({
|
||||||
width: $getTableCellColumnWidth(context.editor, cell),
|
width: $getTableCellColumnWidth(context.editor, cell),
|
||||||
height: styles.get('height') || '',
|
height: styles.get('height') || '',
|
||||||
type: cell.getTag(),
|
type: cell.getTag(),
|
||||||
h_align: cell.getFormatType(),
|
h_align: cell.getAlignment(),
|
||||||
v_align: styles.get('vertical-align') || '',
|
v_align: styles.get('vertical-align') || '',
|
||||||
border_width: styles.get('border-width') || '',
|
border_width: styles.get('border-width') || '',
|
||||||
border_style: styles.get('border-style') || '',
|
border_style: styles.get('border-style') || '',
|
||||||
|
@ -89,7 +88,7 @@ export const cellProperties: EditorFormDefinition = {
|
||||||
|
|
||||||
$setTableCellColumnWidth(cell, width);
|
$setTableCellColumnWidth(cell, width);
|
||||||
cell.updateTag(formData.get('type')?.toString() || '');
|
cell.updateTag(formData.get('type')?.toString() || '');
|
||||||
cell.setFormat((formData.get('h_align')?.toString() || '') as ElementFormatType);
|
cell.setAlignment((formData.get('h_align')?.toString() || '') as CommonBlockAlignment);
|
||||||
|
|
||||||
const styles = cell.getStyles();
|
const styles = cell.getStyles();
|
||||||
styles.set('height', formatSizeValue(formData.get('height')?.toString() || ''));
|
styles.set('height', formatSizeValue(formData.get('height')?.toString() || ''));
|
||||||
|
@ -172,7 +171,7 @@ export const cellProperties: EditorFormDefinition = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function $showRowPropertiesForm(row: CustomTableRowNode, context: EditorUiContext): EditorFormModal {
|
export function $showRowPropertiesForm(row: TableRowNode, context: EditorUiContext): EditorFormModal {
|
||||||
const styles = row.getStyles();
|
const styles = row.getStyles();
|
||||||
const modalForm = context.manager.createModal('row_properties');
|
const modalForm = context.manager.createModal('row_properties');
|
||||||
modalForm.show({
|
modalForm.show({
|
||||||
|
@ -216,7 +215,7 @@ export const rowProperties: EditorFormDefinition = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function $showTablePropertiesForm(table: CustomTableNode, context: EditorUiContext): EditorFormModal {
|
export function $showTablePropertiesForm(table: TableNode, context: EditorUiContext): EditorFormModal {
|
||||||
const styles = table.getStyles();
|
const styles = table.getStyles();
|
||||||
const modalForm = context.manager.createModal('table_properties');
|
const modalForm = context.manager.createModal('table_properties');
|
||||||
modalForm.show({
|
modalForm.show({
|
||||||
|
@ -229,7 +228,7 @@ export function $showTablePropertiesForm(table: CustomTableNode, context: Editor
|
||||||
border_color: styles.get('border-color') || '',
|
border_color: styles.get('border-color') || '',
|
||||||
background_color: styles.get('background-color') || '',
|
background_color: styles.get('background-color') || '',
|
||||||
// caption: '', TODO
|
// caption: '', TODO
|
||||||
align: table.getFormatType(),
|
align: table.getAlignment(),
|
||||||
});
|
});
|
||||||
return modalForm;
|
return modalForm;
|
||||||
}
|
}
|
||||||
|
@ -253,12 +252,12 @@ export const tableProperties: EditorFormDefinition = {
|
||||||
styles.set('background-color', formData.get('background_color')?.toString() || '');
|
styles.set('background-color', formData.get('background_color')?.toString() || '');
|
||||||
table.setStyles(styles);
|
table.setStyles(styles);
|
||||||
|
|
||||||
table.setFormat(formData.get('align') as ElementFormatType);
|
table.setAlignment(formData.get('align') as CommonBlockAlignment);
|
||||||
|
|
||||||
const cellPadding = (formData.get('cell_padding')?.toString() || '');
|
const cellPadding = (formData.get('cell_padding')?.toString() || '');
|
||||||
if (cellPadding) {
|
if (cellPadding) {
|
||||||
const cellPaddingFormatted = formatSizeValue(cellPadding);
|
const cellPaddingFormatted = formatSizeValue(cellPadding);
|
||||||
$forEachTableCell(table, (cell: CustomTableCellNode) => {
|
$forEachTableCell(table, (cell: TableCellNode) => {
|
||||||
const styles = cell.getStyles();
|
const styles = cell.getStyles();
|
||||||
styles.set('padding', cellPaddingFormatted);
|
styles.set('padding', cellPaddingFormatted);
|
||||||
cell.setStyles(styles);
|
cell.setStyles(styles);
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import {EditorContainerUiElement} from "../core";
|
import {EditorContainerUiElement} from "../core";
|
||||||
import {el} from "../../../utils/dom";
|
import {el} from "../../../utils/dom";
|
||||||
import {EditorFormField} from "../forms";
|
import {EditorFormField} from "../forms";
|
||||||
import {CustomHeadingNode} from "../../../nodes/custom-heading";
|
|
||||||
import {$getAllNodesOfType} from "../../../utils/nodes";
|
import {$getAllNodesOfType} from "../../../utils/nodes";
|
||||||
import {$isHeadingNode} from "@lexical/rich-text";
|
|
||||||
import {uniqueIdSmall} from "../../../../services/util";
|
import {uniqueIdSmall} from "../../../../services/util";
|
||||||
|
import {$isHeadingNode, HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
|
||||||
|
|
||||||
export class LinkField extends EditorContainerUiElement {
|
export class LinkField extends EditorContainerUiElement {
|
||||||
protected input: EditorFormField;
|
protected input: EditorFormField;
|
||||||
protected headerMap = new Map<string, CustomHeadingNode>();
|
protected headerMap = new Map<string, HeadingNode>();
|
||||||
|
|
||||||
constructor(input: EditorFormField) {
|
constructor(input: EditorFormField) {
|
||||||
super([input]);
|
super([input]);
|
||||||
|
@ -43,7 +42,7 @@ export class LinkField extends EditorContainerUiElement {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFormFromHeader(header: CustomHeadingNode) {
|
updateFormFromHeader(header: HeadingNode) {
|
||||||
this.getHeaderIdAndText(header).then(({id, text}) => {
|
this.getHeaderIdAndText(header).then(({id, text}) => {
|
||||||
console.log('updating form', id, text);
|
console.log('updating form', id, text);
|
||||||
const modal = this.getContext().manager.getActiveModal('link');
|
const modal = this.getContext().manager.getActiveModal('link');
|
||||||
|
@ -57,7 +56,7 @@ export class LinkField extends EditorContainerUiElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getHeaderIdAndText(header: CustomHeadingNode): Promise<{id: string, text: string}> {
|
getHeaderIdAndText(header: HeadingNode): Promise<{id: string, text: string}> {
|
||||||
return new Promise((res) => {
|
return new Promise((res) => {
|
||||||
this.getContext().editor.update(() => {
|
this.getContext().editor.update(() => {
|
||||||
let id = header.getId();
|
let id = header.getId();
|
||||||
|
@ -75,7 +74,7 @@ export class LinkField extends EditorContainerUiElement {
|
||||||
|
|
||||||
updateDataList(listEl: HTMLElement) {
|
updateDataList(listEl: HTMLElement) {
|
||||||
this.getContext().editor.getEditorState().read(() => {
|
this.getContext().editor.getEditorState().read(() => {
|
||||||
const headers = $getAllNodesOfType($isHeadingNode) as CustomHeadingNode[];
|
const headers = $getAllNodesOfType($isHeadingNode) as HeadingNode[];
|
||||||
|
|
||||||
this.headerMap.clear();
|
this.headerMap.clear();
|
||||||
const listEls: HTMLElement[] = [];
|
const listEls: HTMLElement[] = [];
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {EditorUiElement} from "../core";
|
import {EditorUiElement} from "../core";
|
||||||
import {$createTableNodeWithDimensions} from "@lexical/table";
|
import {$createTableNodeWithDimensions} from "@lexical/table";
|
||||||
import {CustomTableNode} from "../../../nodes/custom-table";
|
|
||||||
import {$insertNewBlockNodeAtSelection} from "../../../utils/selection";
|
import {$insertNewBlockNodeAtSelection} from "../../../utils/selection";
|
||||||
import {el} from "../../../utils/dom";
|
import {el} from "../../../utils/dom";
|
||||||
|
|
||||||
|
@ -78,7 +77,7 @@ export class EditorTableCreator extends EditorUiElement {
|
||||||
const colWidths = Array(columns).fill(targetColWidth + 'px');
|
const colWidths = Array(columns).fill(targetColWidth + 'px');
|
||||||
|
|
||||||
this.getContext().editor.update(() => {
|
this.getContext().editor.update(() => {
|
||||||
const table = $createTableNodeWithDimensions(rows, columns, false) as CustomTableNode;
|
const table = $createTableNodeWithDimensions(rows, columns, false);
|
||||||
table.setColWidths(colWidths);
|
table.setColWidths(colWidths);
|
||||||
$insertNewBlockNodeAtSelection(table);
|
$insertNewBlockNodeAtSelection(table);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import {BaseSelection, LexicalNode,} from "lexical";
|
import {BaseSelection, LexicalNode,} from "lexical";
|
||||||
import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker";
|
import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker";
|
||||||
import {el} from "../../../utils/dom";
|
import {el} from "../../../utils/dom";
|
||||||
import {$isImageNode} from "../../../nodes/image";
|
import {$isImageNode} from "@lexical/rich-text/LexicalImageNode";
|
||||||
import {EditorUiContext} from "../core";
|
import {EditorUiContext} from "../core";
|
||||||
import {NodeHasSize} from "../../../nodes/_common";
|
import {NodeHasSize} from "lexical/nodes/common";
|
||||||
import {$isMediaNode} from "../../../nodes/media";
|
import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode";
|
||||||
|
|
||||||
function isNodeWithSize(node: LexicalNode): node is NodeHasSize&LexicalNode {
|
function isNodeWithSize(node: LexicalNode): node is NodeHasSize&LexicalNode {
|
||||||
return $isImageNode(node) || $isMediaNode(node);
|
return $isImageNode(node) || $isMediaNode(node);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical";
|
import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical";
|
||||||
import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker";
|
import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker";
|
||||||
import {CustomTableNode} from "../../../nodes/custom-table";
|
import {TableNode, TableRowNode} from "@lexical/table";
|
||||||
import {TableRowNode} from "@lexical/table";
|
|
||||||
import {el} from "../../../utils/dom";
|
import {el} from "../../../utils/dom";
|
||||||
import {$getTableColumnWidth, $setTableColumnWidth} from "../../../utils/tables";
|
import {$getTableColumnWidth, $setTableColumnWidth} from "../../../utils/tables";
|
||||||
|
|
||||||
|
@ -148,7 +147,7 @@ class TableResizer {
|
||||||
|
|
||||||
_this.editor.update(() => {
|
_this.editor.update(() => {
|
||||||
const table = $getNearestNodeFromDOMNode(parentTable);
|
const table = $getNearestNodeFromDOMNode(parentTable);
|
||||||
if (table instanceof CustomTableNode) {
|
if (table instanceof TableNode) {
|
||||||
const originalWidth = $getTableColumnWidth(_this.editor, table, cellIndex);
|
const originalWidth = $getTableColumnWidth(_this.editor, table, cellIndex);
|
||||||
const newWidth = Math.max(originalWidth + change, 10);
|
const newWidth = Math.max(originalWidth + change, 10);
|
||||||
$setTableColumnWidth(table, cellIndex, newWidth);
|
$setTableColumnWidth(table, cellIndex, newWidth);
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import {$getNodeByKey, LexicalEditor} from "lexical";
|
import {$getNodeByKey, LexicalEditor} from "lexical";
|
||||||
import {NodeKey} from "lexical/LexicalNode";
|
import {NodeKey} from "lexical/LexicalNode";
|
||||||
import {
|
import {
|
||||||
|
$isTableNode,
|
||||||
applyTableHandlers,
|
applyTableHandlers,
|
||||||
HTMLTableElementWithWithTableSelectionState,
|
HTMLTableElementWithWithTableSelectionState,
|
||||||
TableNode,
|
TableNode,
|
||||||
TableObserver
|
TableObserver
|
||||||
} from "@lexical/table";
|
} from "@lexical/table";
|
||||||
import {$isCustomTableNode, CustomTableNode} from "../../../nodes/custom-table";
|
|
||||||
|
|
||||||
// File adapted from logic in:
|
// File adapted from logic in:
|
||||||
// https://github.com/facebook/lexical/blob/f373759a7849f473d34960a6bf4e34b2a011e762/packages/lexical-react/src/LexicalTablePlugin.ts#L49
|
// https://github.com/facebook/lexical/blob/f373759a7849f473d34960a6bf4e34b2a011e762/packages/lexical-react/src/LexicalTablePlugin.ts#L49
|
||||||
|
@ -25,12 +25,12 @@ class TableSelectionHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected init() {
|
protected init() {
|
||||||
this.unregisterMutationListener = this.editor.registerMutationListener(CustomTableNode, (mutations) => {
|
this.unregisterMutationListener = this.editor.registerMutationListener(TableNode, (mutations) => {
|
||||||
for (const [nodeKey, mutation] of mutations) {
|
for (const [nodeKey, mutation] of mutations) {
|
||||||
if (mutation === 'created') {
|
if (mutation === 'created') {
|
||||||
this.editor.getEditorState().read(() => {
|
this.editor.getEditorState().read(() => {
|
||||||
const tableNode = $getNodeByKey<CustomTableNode>(nodeKey);
|
const tableNode = $getNodeByKey<TableNode>(nodeKey);
|
||||||
if ($isCustomTableNode(tableNode)) {
|
if ($isTableNode(tableNode)) {
|
||||||
this.initializeTableNode(tableNode);
|
this.initializeTableNode(tableNode);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical";
|
import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical";
|
||||||
import {$isCustomListItemNode} from "../../../nodes/custom-list-item";
|
import {$isListItemNode} from "@lexical/list";
|
||||||
|
|
||||||
class TaskListHandler {
|
class TaskListHandler {
|
||||||
protected editorContainer: HTMLElement;
|
protected editorContainer: HTMLElement;
|
||||||
|
@ -38,7 +38,7 @@ class TaskListHandler {
|
||||||
|
|
||||||
this.editor.update(() => {
|
this.editor.update(() => {
|
||||||
const node = $getNearestNodeFromDOMNode(listItem);
|
const node = $getNearestNodeFromDOMNode(listItem);
|
||||||
if ($isCustomListItemNode(node)) {
|
if ($isListItemNode(node)) {
|
||||||
node.setChecked(!node.getChecked());
|
node.setChecked(!node.getChecked());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {EditorFormModal, EditorFormModalDefinition} from "./modals";
|
import {EditorFormModal, EditorFormModalDefinition} from "./modals";
|
||||||
import {EditorContainerUiElement, EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core";
|
import {EditorContainerUiElement, EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core";
|
||||||
import {EditorDecorator, EditorDecoratorAdapter} from "./decorator";
|
import {EditorDecorator, EditorDecoratorAdapter} from "./decorator";
|
||||||
import {$getSelection, BaseSelection, COMMAND_PRIORITY_LOW, LexicalEditor, SELECTION_CHANGE_COMMAND} from "lexical";
|
import {BaseSelection, LexicalEditor} from "lexical";
|
||||||
import {DecoratorListener} from "lexical/LexicalEditor";
|
import {DecoratorListener} from "lexical/LexicalEditor";
|
||||||
import type {NodeKey} from "lexical/LexicalNode";
|
import type {NodeKey} from "lexical/LexicalNode";
|
||||||
import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars";
|
import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars";
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import {$getSelection, $insertNodes, LexicalEditor, LexicalNode} from "lexical";
|
import {$insertNodes, LexicalEditor, LexicalNode} from "lexical";
|
||||||
import {HttpError} from "../../services/http";
|
import {HttpError} from "../../services/http";
|
||||||
import {EditorUiContext} from "../ui/framework/core";
|
import {EditorUiContext} from "../ui/framework/core";
|
||||||
import * as DrawIO from "../../services/drawio";
|
import * as DrawIO from "../../services/drawio";
|
||||||
import {$createDiagramNode, DiagramNode} from "../nodes/diagram";
|
import {$createDiagramNode, DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
|
||||||
import {ImageManager} from "../../components";
|
import {ImageManager} from "../../components";
|
||||||
import {EditorImageData} from "./images";
|
import {EditorImageData} from "./images";
|
||||||
import {$getNodeFromSelection, getLastSelection} from "./selection";
|
import {$getNodeFromSelection, getLastSelection} from "./selection";
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
import {$isQuoteNode, HeadingNode, HeadingTagType} from "@lexical/rich-text";
|
import {
|
||||||
import {$createTextNode, $getSelection, $insertNodes, LexicalEditor, LexicalNode} from "lexical";
|
$createParagraphNode,
|
||||||
|
$createTextNode,
|
||||||
|
$getSelection,
|
||||||
|
$insertNodes,
|
||||||
|
$isParagraphNode,
|
||||||
|
LexicalEditor,
|
||||||
|
LexicalNode
|
||||||
|
} from "lexical";
|
||||||
import {
|
import {
|
||||||
$getBlockElementNodesInSelection,
|
$getBlockElementNodesInSelection,
|
||||||
$getNodeFromSelection,
|
$getNodeFromSelection,
|
||||||
|
@ -7,37 +14,35 @@ import {
|
||||||
$toggleSelectionBlockNodeType,
|
$toggleSelectionBlockNodeType,
|
||||||
getLastSelection
|
getLastSelection
|
||||||
} from "./selection";
|
} from "./selection";
|
||||||
import {$createCustomHeadingNode, $isCustomHeadingNode} from "../nodes/custom-heading";
|
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "@lexical/rich-text/LexicalCodeBlockNode";
|
||||||
import {$createCustomParagraphNode, $isCustomParagraphNode} from "../nodes/custom-paragraph";
|
import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "@lexical/rich-text/LexicalCalloutNode";
|
||||||
import {$createCustomQuoteNode} from "../nodes/custom-quote";
|
import {$isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list";
|
||||||
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../nodes/code-block";
|
|
||||||
import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "../nodes/callout";
|
|
||||||
import {insertList, ListNode, ListType, removeList} from "@lexical/list";
|
|
||||||
import {$isCustomListNode} from "../nodes/custom-list";
|
|
||||||
import {$createLinkNode, $isLinkNode} from "@lexical/link";
|
import {$createLinkNode, $isLinkNode} from "@lexical/link";
|
||||||
|
import {$createHeadingNode, $isHeadingNode, HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode";
|
||||||
|
import {$createQuoteNode, $isQuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
|
||||||
|
|
||||||
const $isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
|
const $isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
|
||||||
return $isCustomHeadingNode(node) && (node as HeadingNode).getTag() === tag;
|
return $isHeadingNode(node) && node.getTag() === tag;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function toggleSelectionAsHeading(editor: LexicalEditor, tag: HeadingTagType) {
|
export function toggleSelectionAsHeading(editor: LexicalEditor, tag: HeadingTagType) {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
$toggleSelectionBlockNodeType(
|
$toggleSelectionBlockNodeType(
|
||||||
(node) => $isHeaderNodeOfTag(node, tag),
|
(node) => $isHeaderNodeOfTag(node, tag),
|
||||||
() => $createCustomHeadingNode(tag),
|
() => $createHeadingNode(tag),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleSelectionAsParagraph(editor: LexicalEditor) {
|
export function toggleSelectionAsParagraph(editor: LexicalEditor) {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
$toggleSelectionBlockNodeType($isCustomParagraphNode, $createCustomParagraphNode);
|
$toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleSelectionAsBlockquote(editor: LexicalEditor) {
|
export function toggleSelectionAsBlockquote(editor: LexicalEditor) {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
$toggleSelectionBlockNodeType($isQuoteNode, $createCustomQuoteNode);
|
$toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +50,7 @@ export function toggleSelectionAsList(editor: LexicalEditor, type: ListType) {
|
||||||
editor.getEditorState().read(() => {
|
editor.getEditorState().read(() => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection();
|
||||||
const listSelected = $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
|
const listSelected = $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
|
||||||
return $isCustomListNode(node) && (node as ListNode).getListType() === type;
|
return $isListNode(node) && (node as ListNode).getListType() === type;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (listSelected) {
|
if (listSelected) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {ImageManager} from "../../components";
|
import {ImageManager} from "../../components";
|
||||||
import {$createImageNode} from "../nodes/image";
|
import {$createImageNode} from "@lexical/rich-text/LexicalImageNode";
|
||||||
import {$createLinkNode, LinkNode} from "@lexical/link";
|
import {$createLinkNode, LinkNode} from "@lexical/link";
|
||||||
|
|
||||||
export type EditorImageData = {
|
export type EditorImageData = {
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
import {$createCustomListItemNode, $isCustomListItemNode, CustomListItemNode} from "../nodes/custom-list-item";
|
|
||||||
import {$createCustomListNode, $isCustomListNode} from "../nodes/custom-list";
|
|
||||||
import {$getSelection, BaseSelection, LexicalEditor} from "lexical";
|
import {$getSelection, BaseSelection, LexicalEditor} from "lexical";
|
||||||
import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection";
|
import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection";
|
||||||
import {nodeHasInset} from "./nodes";
|
import {nodeHasInset} from "./nodes";
|
||||||
|
import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list";
|
||||||
|
|
||||||
|
|
||||||
export function $nestListItem(node: CustomListItemNode): CustomListItemNode {
|
export function $nestListItem(node: ListItemNode): ListItemNode {
|
||||||
const list = node.getParent();
|
const list = node.getParent();
|
||||||
if (!$isCustomListNode(list)) {
|
if (!$isListNode(list)) {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
const listItems = list.getChildren() as CustomListItemNode[];
|
const listItems = list.getChildren() as ListItemNode[];
|
||||||
const nodeIndex = listItems.findIndex((n) => n.getKey() === node.getKey());
|
const nodeIndex = listItems.findIndex((n) => n.getKey() === node.getKey());
|
||||||
const isFirst = nodeIndex === 0;
|
const isFirst = nodeIndex === 0;
|
||||||
|
|
||||||
const newListItem = $createCustomListItemNode();
|
const newListItem = $createListItemNode();
|
||||||
const newList = $createCustomListNode(list.getListType());
|
const newList = $createListNode(list.getListType());
|
||||||
newList.append(newListItem);
|
newList.append(newListItem);
|
||||||
newListItem.append(...node.getChildren());
|
newListItem.append(...node.getChildren());
|
||||||
|
|
||||||
|
@ -31,11 +30,11 @@ export function $nestListItem(node: CustomListItemNode): CustomListItemNode {
|
||||||
return newListItem;
|
return newListItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $unnestListItem(node: CustomListItemNode): CustomListItemNode {
|
export function $unnestListItem(node: ListItemNode): ListItemNode {
|
||||||
const list = node.getParent();
|
const list = node.getParent();
|
||||||
const parentListItem = list?.getParent();
|
const parentListItem = list?.getParent();
|
||||||
const outerList = parentListItem?.getParent();
|
const outerList = parentListItem?.getParent();
|
||||||
if (!$isCustomListNode(list) || !$isCustomListNode(outerList) || !$isCustomListItemNode(parentListItem)) {
|
if (!$isListNode(list) || !$isListNode(outerList) || !$isListItemNode(parentListItem)) {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,19 +50,19 @@ export function $unnestListItem(node: CustomListItemNode): CustomListItemNode {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getListItemsForSelection(selection: BaseSelection|null): (CustomListItemNode|null)[] {
|
function getListItemsForSelection(selection: BaseSelection|null): (ListItemNode|null)[] {
|
||||||
const nodes = selection?.getNodes() || [];
|
const nodes = selection?.getNodes() || [];
|
||||||
const listItemNodes = [];
|
const listItemNodes = [];
|
||||||
|
|
||||||
outer: for (const node of nodes) {
|
outer: for (const node of nodes) {
|
||||||
if ($isCustomListItemNode(node)) {
|
if ($isListItemNode(node)) {
|
||||||
listItemNodes.push(node);
|
listItemNodes.push(node);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parents = node.getParents();
|
const parents = node.getParents();
|
||||||
for (const parent of parents) {
|
for (const parent of parents) {
|
||||||
if ($isCustomListItemNode(parent)) {
|
if ($isListItemNode(parent)) {
|
||||||
listItemNodes.push(parent);
|
listItemNodes.push(parent);
|
||||||
continue outer;
|
continue outer;
|
||||||
}
|
}
|
||||||
|
@ -75,8 +74,8 @@ function getListItemsForSelection(selection: BaseSelection|null): (CustomListIte
|
||||||
return listItemNodes;
|
return listItemNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function $reduceDedupeListItems(listItems: (CustomListItemNode|null)[]): CustomListItemNode[] {
|
function $reduceDedupeListItems(listItems: (ListItemNode|null)[]): ListItemNode[] {
|
||||||
const listItemMap: Record<string, CustomListItemNode> = {};
|
const listItemMap: Record<string, ListItemNode> = {};
|
||||||
|
|
||||||
for (const item of listItems) {
|
for (const item of listItems) {
|
||||||
if (item === null) {
|
if (item === null) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
$createParagraphNode,
|
||||||
$getRoot,
|
$getRoot,
|
||||||
$isDecoratorNode,
|
$isDecoratorNode,
|
||||||
$isElementNode, $isRootNode,
|
$isElementNode, $isRootNode,
|
||||||
|
@ -8,16 +9,15 @@ import {
|
||||||
LexicalNode
|
LexicalNode
|
||||||
} from "lexical";
|
} from "lexical";
|
||||||
import {LexicalNodeMatcher} from "../nodes";
|
import {LexicalNodeMatcher} from "../nodes";
|
||||||
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
|
|
||||||
import {$generateNodesFromDOM} from "@lexical/html";
|
import {$generateNodesFromDOM} from "@lexical/html";
|
||||||
import {htmlToDom} from "./dom";
|
import {htmlToDom} from "./dom";
|
||||||
import {NodeHasAlignment, NodeHasInset} from "../nodes/_common";
|
import {NodeHasAlignment, NodeHasInset} from "lexical/nodes/common";
|
||||||
import {$findMatchingParent} from "@lexical/utils";
|
import {$findMatchingParent} from "@lexical/utils";
|
||||||
|
|
||||||
function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] {
|
function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] {
|
||||||
return nodes.map(node => {
|
return nodes.map(node => {
|
||||||
if ($isTextNode(node)) {
|
if ($isTextNode(node)) {
|
||||||
const paragraph = $createCustomParagraphNode();
|
const paragraph = $createParagraphNode();
|
||||||
paragraph.append(node);
|
paragraph.append(node);
|
||||||
return paragraph;
|
return paragraph;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,18 +7,16 @@ import {
|
||||||
$isTextNode,
|
$isTextNode,
|
||||||
$setSelection,
|
$setSelection,
|
||||||
BaseSelection, DecoratorNode,
|
BaseSelection, DecoratorNode,
|
||||||
ElementFormatType,
|
|
||||||
ElementNode, LexicalEditor,
|
ElementNode, LexicalEditor,
|
||||||
LexicalNode,
|
LexicalNode,
|
||||||
TextFormatType, TextNode
|
TextFormatType, TextNode
|
||||||
} from "lexical";
|
} from "lexical";
|
||||||
import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
|
import {$getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
|
||||||
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes";
|
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes";
|
||||||
import {$setBlocksType} from "@lexical/selection";
|
import {$setBlocksType} from "@lexical/selection";
|
||||||
|
|
||||||
import {$getNearestNodeBlockParent, $getParentOfType, nodeHasAlignment} from "./nodes";
|
import {$getNearestNodeBlockParent, $getParentOfType, nodeHasAlignment} from "./nodes";
|
||||||
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
|
import {CommonBlockAlignment} from "lexical/nodes/common";
|
||||||
import {CommonBlockAlignment} from "../nodes/_common";
|
|
||||||
|
|
||||||
const lastSelectionByEditor = new WeakMap<LexicalEditor, BaseSelection|null>;
|
const lastSelectionByEditor = new WeakMap<LexicalEditor, BaseSelection|null>;
|
||||||
|
|
||||||
|
@ -71,7 +69,7 @@ export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creat
|
||||||
const selection = $getSelection();
|
const selection = $getSelection();
|
||||||
const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
|
const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
|
||||||
if (selection && matcher(blockElement)) {
|
if (selection && matcher(blockElement)) {
|
||||||
$setBlocksType(selection, $createCustomParagraphNode);
|
$setBlocksType(selection, $createParagraphNode);
|
||||||
} else {
|
} else {
|
||||||
$setBlocksType(selection, creator);
|
$setBlocksType(selection, creator);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,28 @@
|
||||||
import {NodeClipboard} from "./node-clipboard";
|
import {NodeClipboard} from "./node-clipboard";
|
||||||
import {CustomTableRowNode} from "../nodes/custom-table-row";
|
|
||||||
import {$getTableCellsFromSelection, $getTableFromSelection, $getTableRowsFromSelection} from "./tables";
|
import {$getTableCellsFromSelection, $getTableFromSelection, $getTableRowsFromSelection} from "./tables";
|
||||||
import {$getSelection, BaseSelection, LexicalEditor} from "lexical";
|
import {$getSelection, BaseSelection, LexicalEditor} from "lexical";
|
||||||
import {$createCustomTableCellNode, $isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell";
|
|
||||||
import {CustomTableNode} from "../nodes/custom-table";
|
|
||||||
import {TableMap} from "./table-map";
|
import {TableMap} from "./table-map";
|
||||||
import {$isTableSelection} from "@lexical/table";
|
import {
|
||||||
|
$createTableCellNode,
|
||||||
|
$isTableCellNode,
|
||||||
|
$isTableSelection,
|
||||||
|
TableCellNode,
|
||||||
|
TableNode,
|
||||||
|
TableRowNode
|
||||||
|
} from "@lexical/table";
|
||||||
import {$getNodeFromSelection} from "./selection";
|
import {$getNodeFromSelection} from "./selection";
|
||||||
|
|
||||||
const rowClipboard: NodeClipboard<CustomTableRowNode> = new NodeClipboard<CustomTableRowNode>();
|
const rowClipboard: NodeClipboard<TableRowNode> = new NodeClipboard<TableRowNode>();
|
||||||
|
|
||||||
export function isRowClipboardEmpty(): boolean {
|
export function isRowClipboardEmpty(): boolean {
|
||||||
return rowClipboard.size() === 0;
|
return rowClipboard.size() === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateRowsToCopy(rows: CustomTableRowNode[]): void {
|
export function validateRowsToCopy(rows: TableRowNode[]): void {
|
||||||
let commonRowSize: number|null = null;
|
let commonRowSize: number|null = null;
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const cells = row.getChildren().filter(n => $isCustomTableCellNode(n));
|
const cells = row.getChildren().filter(n => $isTableCellNode(n));
|
||||||
let rowSize = 0;
|
let rowSize = 0;
|
||||||
for (const cell of cells) {
|
for (const cell of cells) {
|
||||||
rowSize += cell.getColSpan() || 1;
|
rowSize += cell.getColSpan() || 1;
|
||||||
|
@ -35,10 +39,10 @@ export function validateRowsToCopy(rows: CustomTableRowNode[]): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateRowsToPaste(rows: CustomTableRowNode[], targetTable: CustomTableNode): void {
|
export function validateRowsToPaste(rows: TableRowNode[], targetTable: TableNode): void {
|
||||||
const tableColCount = (new TableMap(targetTable)).columnCount;
|
const tableColCount = (new TableMap(targetTable)).columnCount;
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const cells = row.getChildren().filter(n => $isCustomTableCellNode(n));
|
const cells = row.getChildren().filter(n => $isTableCellNode(n));
|
||||||
let rowSize = 0;
|
let rowSize = 0;
|
||||||
for (const cell of cells) {
|
for (const cell of cells) {
|
||||||
rowSize += cell.getColSpan() || 1;
|
rowSize += cell.getColSpan() || 1;
|
||||||
|
@ -49,7 +53,7 @@ export function validateRowsToPaste(rows: CustomTableRowNode[], targetTable: Cus
|
||||||
}
|
}
|
||||||
|
|
||||||
while (rowSize < tableColCount) {
|
while (rowSize < tableColCount) {
|
||||||
row.append($createCustomTableCellNode());
|
row.append($createTableCellNode());
|
||||||
rowSize++;
|
rowSize++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,11 +102,11 @@ export function $pasteClipboardRowsAfter(editor: LexicalEditor): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnClipboard: NodeClipboard<CustomTableCellNode>[] = [];
|
const columnClipboard: NodeClipboard<TableCellNode>[] = [];
|
||||||
|
|
||||||
function setColumnClipboard(columns: CustomTableCellNode[][]): void {
|
function setColumnClipboard(columns: TableCellNode[][]): void {
|
||||||
const newClipboards = columns.map(cells => {
|
const newClipboards = columns.map(cells => {
|
||||||
const clipboard = new NodeClipboard<CustomTableCellNode>();
|
const clipboard = new NodeClipboard<TableCellNode>();
|
||||||
clipboard.set(...cells);
|
clipboard.set(...cells);
|
||||||
return clipboard;
|
return clipboard;
|
||||||
});
|
});
|
||||||
|
@ -122,9 +126,9 @@ function $getSelectionColumnRange(selection: BaseSelection|null): TableRange|nul
|
||||||
return {from: shape.fromX, to: shape.toX};
|
return {from: shape.fromX, to: shape.toX};
|
||||||
}
|
}
|
||||||
|
|
||||||
const cell = $getNodeFromSelection(selection, $isCustomTableCellNode);
|
const cell = $getNodeFromSelection(selection, $isTableCellNode);
|
||||||
const table = $getTableFromSelection(selection);
|
const table = $getTableFromSelection(selection);
|
||||||
if (!$isCustomTableCellNode(cell) || !table) {
|
if (!$isTableCellNode(cell) || !table) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +141,7 @@ function $getSelectionColumnRange(selection: BaseSelection|null): TableRange|nul
|
||||||
return {from: range.fromX, to: range.toX};
|
return {from: range.fromX, to: range.toX};
|
||||||
}
|
}
|
||||||
|
|
||||||
function $getTableColumnCellsFromSelection(range: TableRange, table: CustomTableNode): CustomTableCellNode[][] {
|
function $getTableColumnCellsFromSelection(range: TableRange, table: TableNode): TableCellNode[][] {
|
||||||
const map = new TableMap(table);
|
const map = new TableMap(table);
|
||||||
const columns = [];
|
const columns = [];
|
||||||
for (let x = range.from; x <= range.to; x++) {
|
for (let x = range.from; x <= range.to; x++) {
|
||||||
|
@ -148,7 +152,7 @@ function $getTableColumnCellsFromSelection(range: TableRange, table: CustomTable
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateColumnsToCopy(columns: CustomTableCellNode[][]): void {
|
function validateColumnsToCopy(columns: TableCellNode[][]): void {
|
||||||
let commonColSize: number|null = null;
|
let commonColSize: number|null = null;
|
||||||
|
|
||||||
for (const cells of columns) {
|
for (const cells of columns) {
|
||||||
|
@ -203,7 +207,7 @@ export function $copySelectedColumnsToClipboard(): void {
|
||||||
setColumnClipboard(columns);
|
setColumnClipboard(columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateColumnsToPaste(columns: CustomTableCellNode[][], targetTable: CustomTableNode) {
|
function validateColumnsToPaste(columns: TableCellNode[][], targetTable: TableNode) {
|
||||||
const tableRowCount = (new TableMap(targetTable)).rowCount;
|
const tableRowCount = (new TableMap(targetTable)).rowCount;
|
||||||
for (const cells of columns) {
|
for (const cells of columns) {
|
||||||
let colSize = 0;
|
let colSize = 0;
|
||||||
|
@ -216,7 +220,7 @@ function validateColumnsToPaste(columns: CustomTableCellNode[][], targetTable: C
|
||||||
}
|
}
|
||||||
|
|
||||||
while (colSize < tableRowCount) {
|
while (colSize < tableRowCount) {
|
||||||
cells.push($createCustomTableCellNode());
|
cells.push($createTableCellNode());
|
||||||
colSize++;
|
colSize++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import {CustomTableNode} from "../nodes/custom-table";
|
import {$isTableCellNode, $isTableRowNode, TableCellNode, TableNode} from "@lexical/table";
|
||||||
import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell";
|
|
||||||
import {$isTableRowNode} from "@lexical/table";
|
|
||||||
|
|
||||||
export type CellRange = {
|
export type CellRange = {
|
||||||
fromX: number;
|
fromX: number;
|
||||||
|
@ -16,15 +14,15 @@ export class TableMap {
|
||||||
|
|
||||||
// Represents an array (rows*columns in length) of cell nodes from top-left to
|
// Represents an array (rows*columns in length) of cell nodes from top-left to
|
||||||
// bottom right. Cells may repeat where merged and covering multiple spaces.
|
// bottom right. Cells may repeat where merged and covering multiple spaces.
|
||||||
cells: CustomTableCellNode[] = [];
|
cells: TableCellNode[] = [];
|
||||||
|
|
||||||
constructor(table: CustomTableNode) {
|
constructor(table: TableNode) {
|
||||||
this.buildCellMap(table);
|
this.buildCellMap(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected buildCellMap(table: CustomTableNode) {
|
protected buildCellMap(table: TableNode) {
|
||||||
const rowsAndCells: CustomTableCellNode[][] = [];
|
const rowsAndCells: TableCellNode[][] = [];
|
||||||
const setCell = (x: number, y: number, cell: CustomTableCellNode) => {
|
const setCell = (x: number, y: number, cell: TableCellNode) => {
|
||||||
if (typeof rowsAndCells[y] === 'undefined') {
|
if (typeof rowsAndCells[y] === 'undefined') {
|
||||||
rowsAndCells[y] = [];
|
rowsAndCells[y] = [];
|
||||||
}
|
}
|
||||||
|
@ -36,7 +34,7 @@ export class TableMap {
|
||||||
const rowNodes = table.getChildren().filter(r => $isTableRowNode(r));
|
const rowNodes = table.getChildren().filter(r => $isTableRowNode(r));
|
||||||
for (let rowIndex = 0; rowIndex < rowNodes.length; rowIndex++) {
|
for (let rowIndex = 0; rowIndex < rowNodes.length; rowIndex++) {
|
||||||
const rowNode = rowNodes[rowIndex];
|
const rowNode = rowNodes[rowIndex];
|
||||||
const cellNodes = rowNode.getChildren().filter(c => $isCustomTableCellNode(c));
|
const cellNodes = rowNode.getChildren().filter(c => $isTableCellNode(c));
|
||||||
let targetColIndex: number = 0;
|
let targetColIndex: number = 0;
|
||||||
for (let cellIndex = 0; cellIndex < cellNodes.length; cellIndex++) {
|
for (let cellIndex = 0; cellIndex < cellNodes.length; cellIndex++) {
|
||||||
const cellNode = cellNodes[cellIndex];
|
const cellNode = cellNodes[cellIndex];
|
||||||
|
@ -60,7 +58,7 @@ export class TableMap {
|
||||||
this.columnCount = Math.max(...rowsAndCells.map(r => r.length));
|
this.columnCount = Math.max(...rowsAndCells.map(r => r.length));
|
||||||
|
|
||||||
const cells = [];
|
const cells = [];
|
||||||
let lastCell: CustomTableCellNode = rowsAndCells[0][0];
|
let lastCell: TableCellNode = rowsAndCells[0][0];
|
||||||
for (let y = 0; y < this.rowCount; y++) {
|
for (let y = 0; y < this.rowCount; y++) {
|
||||||
for (let x = 0; x < this.columnCount; x++) {
|
for (let x = 0; x < this.columnCount; x++) {
|
||||||
if (!rowsAndCells[y] || !rowsAndCells[y][x]) {
|
if (!rowsAndCells[y] || !rowsAndCells[y][x]) {
|
||||||
|
@ -75,7 +73,7 @@ export class TableMap {
|
||||||
this.cells = cells;
|
this.cells = cells;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCellAtPosition(x: number, y: number): CustomTableCellNode {
|
public getCellAtPosition(x: number, y: number): TableCellNode {
|
||||||
const position = (y * this.columnCount) + x;
|
const position = (y * this.columnCount) + x;
|
||||||
if (position >= this.cells.length) {
|
if (position >= this.cells.length) {
|
||||||
throw new Error(`TableMap Error: Attempted to get cell ${position+1} of ${this.cells.length}`);
|
throw new Error(`TableMap Error: Attempted to get cell ${position+1} of ${this.cells.length}`);
|
||||||
|
@ -84,13 +82,13 @@ export class TableMap {
|
||||||
return this.cells[position];
|
return this.cells[position];
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCellsInRange(range: CellRange): CustomTableCellNode[] {
|
public getCellsInRange(range: CellRange): TableCellNode[] {
|
||||||
const minX = Math.max(Math.min(range.fromX, range.toX), 0);
|
const minX = Math.max(Math.min(range.fromX, range.toX), 0);
|
||||||
const maxX = Math.min(Math.max(range.fromX, range.toX), this.columnCount - 1);
|
const maxX = Math.min(Math.max(range.fromX, range.toX), this.columnCount - 1);
|
||||||
const minY = Math.max(Math.min(range.fromY, range.toY), 0);
|
const minY = Math.max(Math.min(range.fromY, range.toY), 0);
|
||||||
const maxY = Math.min(Math.max(range.fromY, range.toY), this.rowCount - 1);
|
const maxY = Math.min(Math.max(range.fromY, range.toY), this.rowCount - 1);
|
||||||
|
|
||||||
const cells = new Set<CustomTableCellNode>();
|
const cells = new Set<TableCellNode>();
|
||||||
|
|
||||||
for (let y = minY; y <= maxY; y++) {
|
for (let y = minY; y <= maxY; y++) {
|
||||||
for (let x = minX; x <= maxX; x++) {
|
for (let x = minX; x <= maxX; x++) {
|
||||||
|
@ -101,7 +99,7 @@ export class TableMap {
|
||||||
return [...cells.values()];
|
return [...cells.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCellsInColumn(columnIndex: number): CustomTableCellNode[] {
|
public getCellsInColumn(columnIndex: number): TableCellNode[] {
|
||||||
return this.getCellsInRange({
|
return this.getCellsInRange({
|
||||||
fromX: columnIndex,
|
fromX: columnIndex,
|
||||||
toX: columnIndex,
|
toX: columnIndex,
|
||||||
|
@ -110,7 +108,7 @@ export class TableMap {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRangeForCell(cell: CustomTableCellNode): CellRange|null {
|
public getRangeForCell(cell: TableCellNode): CellRange|null {
|
||||||
let range: CellRange|null = null;
|
let range: CellRange|null = null;
|
||||||
const cellKey = cell.getKey();
|
const cellKey = cell.getKey();
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
import {BaseSelection, LexicalEditor} from "lexical";
|
import {BaseSelection, LexicalEditor} from "lexical";
|
||||||
import {$isTableRowNode, $isTableSelection, TableRowNode, TableSelection, TableSelectionShape} from "@lexical/table";
|
import {
|
||||||
import {$isCustomTableNode, CustomTableNode} from "../nodes/custom-table";
|
$isTableCellNode,
|
||||||
import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell";
|
$isTableNode,
|
||||||
|
$isTableRowNode,
|
||||||
|
$isTableSelection, TableCellNode, TableNode,
|
||||||
|
TableRowNode,
|
||||||
|
TableSelection,
|
||||||
|
} from "@lexical/table";
|
||||||
import {$getParentOfType} from "./nodes";
|
import {$getParentOfType} from "./nodes";
|
||||||
import {$getNodeFromSelection} from "./selection";
|
import {$getNodeFromSelection} from "./selection";
|
||||||
import {formatSizeValue} from "./dom";
|
import {formatSizeValue} from "./dom";
|
||||||
import {TableMap} from "./table-map";
|
import {TableMap} from "./table-map";
|
||||||
import {$isCustomTableRowNode, CustomTableRowNode} from "../nodes/custom-table-row";
|
|
||||||
|
|
||||||
function $getTableFromCell(cell: CustomTableCellNode): CustomTableNode|null {
|
function $getTableFromCell(cell: TableCellNode): TableNode|null {
|
||||||
return $getParentOfType(cell, $isCustomTableNode) as CustomTableNode|null;
|
return $getParentOfType(cell, $isTableNode) as TableNode|null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTableColumnWidths(table: HTMLTableElement): string[] {
|
export function getTableColumnWidths(table: HTMLTableElement): string[] {
|
||||||
|
@ -55,7 +59,7 @@ function extractWidthFromElement(element: HTMLElement): string {
|
||||||
return width || '';
|
return width || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number, width: number|string): void {
|
export function $setTableColumnWidth(node: TableNode, columnIndex: number, width: number|string): void {
|
||||||
const rows = node.getChildren() as TableRowNode[];
|
const rows = node.getChildren() as TableRowNode[];
|
||||||
let maxCols = 0;
|
let maxCols = 0;
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
@ -78,7 +82,7 @@ export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number,
|
||||||
node.setColWidths(colWidths);
|
node.setColWidths(colWidths);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $getTableColumnWidth(editor: LexicalEditor, node: CustomTableNode, columnIndex: number): number {
|
export function $getTableColumnWidth(editor: LexicalEditor, node: TableNode, columnIndex: number): number {
|
||||||
const colWidths = node.getColWidths();
|
const colWidths = node.getColWidths();
|
||||||
if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) {
|
if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) {
|
||||||
return Number(colWidths[columnIndex].replace('px', ''));
|
return Number(colWidths[columnIndex].replace('px', ''));
|
||||||
|
@ -97,14 +101,14 @@ export function $getTableColumnWidth(editor: LexicalEditor, node: CustomTableNod
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function $getCellColumnIndex(node: CustomTableCellNode): number {
|
function $getCellColumnIndex(node: TableCellNode): number {
|
||||||
const row = node.getParent();
|
const row = node.getParent();
|
||||||
if (!$isTableRowNode(row)) {
|
if (!$isTableRowNode(row)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const cells = row.getChildren<CustomTableCellNode>();
|
const cells = row.getChildren<TableCellNode>();
|
||||||
for (const cell of cells) {
|
for (const cell of cells) {
|
||||||
let colSpan = cell.getColSpan() || 1;
|
let colSpan = cell.getColSpan() || 1;
|
||||||
index += colSpan;
|
index += colSpan;
|
||||||
|
@ -116,7 +120,7 @@ function $getCellColumnIndex(node: CustomTableCellNode): number {
|
||||||
return index - 1;
|
return index - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $setTableCellColumnWidth(cell: CustomTableCellNode, width: string): void {
|
export function $setTableCellColumnWidth(cell: TableCellNode, width: string): void {
|
||||||
const table = $getTableFromCell(cell)
|
const table = $getTableFromCell(cell)
|
||||||
const index = $getCellColumnIndex(cell);
|
const index = $getCellColumnIndex(cell);
|
||||||
|
|
||||||
|
@ -125,7 +129,7 @@ export function $setTableCellColumnWidth(cell: CustomTableCellNode, width: strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $getTableCellColumnWidth(editor: LexicalEditor, cell: CustomTableCellNode): string {
|
export function $getTableCellColumnWidth(editor: LexicalEditor, cell: TableCellNode): string {
|
||||||
const table = $getTableFromCell(cell)
|
const table = $getTableFromCell(cell)
|
||||||
const index = $getCellColumnIndex(cell);
|
const index = $getCellColumnIndex(cell);
|
||||||
if (!table) {
|
if (!table) {
|
||||||
|
@ -136,13 +140,13 @@ export function $getTableCellColumnWidth(editor: LexicalEditor, cell: CustomTabl
|
||||||
return (widths.length > index) ? widths[index] : '';
|
return (widths.length > index) ? widths[index] : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $getTableCellsFromSelection(selection: BaseSelection|null): CustomTableCellNode[] {
|
export function $getTableCellsFromSelection(selection: BaseSelection|null): TableCellNode[] {
|
||||||
if ($isTableSelection(selection)) {
|
if ($isTableSelection(selection)) {
|
||||||
const nodes = selection.getNodes();
|
const nodes = selection.getNodes();
|
||||||
return nodes.filter(n => $isCustomTableCellNode(n));
|
return nodes.filter(n => $isTableCellNode(n));
|
||||||
}
|
}
|
||||||
|
|
||||||
const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as CustomTableCellNode;
|
const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode;
|
||||||
return cell ? [cell] : [];
|
return cell ? [cell] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,12 +197,12 @@ export function $mergeTableCellsInSelection(selection: TableSelection): void {
|
||||||
firstCell.setRowSpan(newHeight);
|
firstCell.setRowSpan(newHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $getTableRowsFromSelection(selection: BaseSelection|null): CustomTableRowNode[] {
|
export function $getTableRowsFromSelection(selection: BaseSelection|null): TableRowNode[] {
|
||||||
const cells = $getTableCellsFromSelection(selection);
|
const cells = $getTableCellsFromSelection(selection);
|
||||||
const rowsByKey: Record<string, CustomTableRowNode> = {};
|
const rowsByKey: Record<string, TableRowNode> = {};
|
||||||
for (const cell of cells) {
|
for (const cell of cells) {
|
||||||
const row = cell.getParent();
|
const row = cell.getParent();
|
||||||
if ($isCustomTableRowNode(row)) {
|
if ($isTableRowNode(row)) {
|
||||||
rowsByKey[row.getKey()] = row;
|
rowsByKey[row.getKey()] = row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -206,28 +210,28 @@ export function $getTableRowsFromSelection(selection: BaseSelection|null): Custo
|
||||||
return Object.values(rowsByKey);
|
return Object.values(rowsByKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $getTableFromSelection(selection: BaseSelection|null): CustomTableNode|null {
|
export function $getTableFromSelection(selection: BaseSelection|null): TableNode|null {
|
||||||
const cells = $getTableCellsFromSelection(selection);
|
const cells = $getTableCellsFromSelection(selection);
|
||||||
if (cells.length === 0) {
|
if (cells.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = $getParentOfType(cells[0], $isCustomTableNode);
|
const table = $getParentOfType(cells[0], $isTableNode);
|
||||||
if ($isCustomTableNode(table)) {
|
if ($isTableNode(table)) {
|
||||||
return table;
|
return table;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $clearTableSizes(table: CustomTableNode): void {
|
export function $clearTableSizes(table: TableNode): void {
|
||||||
table.setColWidths([]);
|
table.setColWidths([]);
|
||||||
|
|
||||||
// TODO - Extra form things once table properties and extra things
|
// TODO - Extra form things once table properties and extra things
|
||||||
// are supported
|
// are supported
|
||||||
|
|
||||||
for (const row of table.getChildren()) {
|
for (const row of table.getChildren()) {
|
||||||
if (!$isCustomTableRowNode(row)) {
|
if (!$isTableRowNode(row)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +240,7 @@ export function $clearTableSizes(table: CustomTableNode): void {
|
||||||
rowStyles.delete('width');
|
rowStyles.delete('width');
|
||||||
row.setStyles(rowStyles);
|
row.setStyles(rowStyles);
|
||||||
|
|
||||||
const cells = row.getChildren().filter(c => $isCustomTableCellNode(c));
|
const cells = row.getChildren().filter(c => $isTableCellNode(c));
|
||||||
for (const cell of cells) {
|
for (const cell of cells) {
|
||||||
const cellStyles = cell.getStyles();
|
const cellStyles = cell.getStyles();
|
||||||
cellStyles.delete('height');
|
cellStyles.delete('height');
|
||||||
|
@ -247,23 +251,21 @@ export function $clearTableSizes(table: CustomTableNode): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $clearTableFormatting(table: CustomTableNode): void {
|
export function $clearTableFormatting(table: TableNode): void {
|
||||||
table.setColWidths([]);
|
table.setColWidths([]);
|
||||||
table.setStyles(new Map);
|
table.setStyles(new Map);
|
||||||
|
|
||||||
for (const row of table.getChildren()) {
|
for (const row of table.getChildren()) {
|
||||||
if (!$isCustomTableRowNode(row)) {
|
if (!$isTableRowNode(row)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
row.setStyles(new Map);
|
row.setStyles(new Map);
|
||||||
row.setFormat('');
|
|
||||||
|
|
||||||
const cells = row.getChildren().filter(c => $isCustomTableCellNode(c));
|
const cells = row.getChildren().filter(c => $isTableCellNode(c));
|
||||||
for (const cell of cells) {
|
for (const cell of cells) {
|
||||||
cell.setStyles(new Map);
|
cell.setStyles(new Map);
|
||||||
cell.clearWidth();
|
cell.clearWidth();
|
||||||
cell.setFormat('');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -272,14 +274,14 @@ export function $clearTableFormatting(table: CustomTableNode): void {
|
||||||
* Perform the given callback for each cell in the given table.
|
* Perform the given callback for each cell in the given table.
|
||||||
* Returning false from the callback stops the function early.
|
* Returning false from the callback stops the function early.
|
||||||
*/
|
*/
|
||||||
export function $forEachTableCell(table: CustomTableNode, callback: (c: CustomTableCellNode) => void|false): void {
|
export function $forEachTableCell(table: TableNode, callback: (c: TableCellNode) => void|false): void {
|
||||||
outer: for (const row of table.getChildren()) {
|
outer: for (const row of table.getChildren()) {
|
||||||
if (!$isCustomTableRowNode(row)) {
|
if (!$isTableRowNode(row)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const cells = row.getChildren();
|
const cells = row.getChildren();
|
||||||
for (const cell of cells) {
|
for (const cell of cells) {
|
||||||
if (!$isCustomTableCellNode(cell)) {
|
if (!$isTableCellNode(cell)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = callback(cell);
|
const result = callback(cell);
|
||||||
|
@ -290,10 +292,10 @@ export function $forEachTableCell(table: CustomTableNode, callback: (c: CustomTa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $getCellPaddingForTable(table: CustomTableNode): string {
|
export function $getCellPaddingForTable(table: TableNode): string {
|
||||||
let padding: string|null = null;
|
let padding: string|null = null;
|
||||||
|
|
||||||
$forEachTableCell(table, (cell: CustomTableCellNode) => {
|
$forEachTableCell(table, (cell: TableCellNode) => {
|
||||||
const cellPadding = cell.getStyles().get('padding') || ''
|
const cellPadding = cell.getStyles().get('padding') || ''
|
||||||
if (padding === null) {
|
if (padding === null) {
|
||||||
padding = cellPadding;
|
padding = cellPadding;
|
||||||
|
|
Loading…
Reference in New Issue