Lexical: Extracted & merged heading & quote nodes
This commit is contained in:
parent
f3fa63a5ae
commit
36a4d79120
|
@ -8,11 +8,12 @@
|
||||||
|
|
||||||
import {$createLinkNode} from '@lexical/link';
|
import {$createLinkNode} from '@lexical/link';
|
||||||
import {$createListItemNode, $createListNode} from '@lexical/list';
|
import {$createListItemNode, $createListNode} from '@lexical/list';
|
||||||
import {$createHeadingNode, $createQuoteNode} from '@lexical/rich-text';
|
|
||||||
import {$createTableNodeWithDimensions} from '@lexical/table';
|
import {$createTableNodeWithDimensions} from '@lexical/table';
|
||||||
import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';
|
import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';
|
||||||
|
|
||||||
import {initializeUnitTest} from '../utils';
|
import {initializeUnitTest} from '../utils';
|
||||||
|
import {$createHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
|
||||||
|
import {$createQuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
|
||||||
|
|
||||||
function $createEditorContent() {
|
function $createEditorContent() {
|
||||||
const root = $getRoot();
|
const root = $getRoot();
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -48,7 +48,7 @@ export class CommonBlockNode extends ElementNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copyCommonBlockProperties(from: CommonBlockNode, to: CommonBlockNode): void {
|
export function copyCommonBlockProperties(from: CommonBlockNode, to: CommonBlockNode): void {
|
||||||
to.__id = from.__id;
|
// to.__id = from.__id;
|
||||||
to.__alignment = from.__alignment;
|
to.__alignment = from.__alignment;
|
||||||
to.__inset = from.__inset;
|
to.__inset = from.__inset;
|
||||||
}
|
}
|
|
@ -11,7 +11,7 @@ import {
|
||||||
$insertDataTransferForRichText,
|
$insertDataTransferForRichText,
|
||||||
} from '@lexical/clipboard';
|
} from '@lexical/clipboard';
|
||||||
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 {
|
||||||
$createParagraphNode,
|
$createParagraphNode,
|
||||||
$createRangeSelection,
|
$createRangeSelection,
|
||||||
|
@ -32,6 +32,7 @@ import {
|
||||||
initializeUnitTest,
|
initializeUnitTest,
|
||||||
invariant,
|
invariant,
|
||||||
} from '../../../__tests__/utils';
|
} from '../../../__tests__/utils';
|
||||||
|
import {$createHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
|
||||||
|
|
||||||
describe('LexicalTabNode tests', () => {
|
describe('LexicalTabNode tests', () => {
|
||||||
initializeUnitTest((testEnv) => {
|
initializeUnitTest((testEnv) => {
|
||||||
|
|
|
@ -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<{
|
||||||
|
|
|
@ -0,0 +1,202 @@
|
||||||
|
import {
|
||||||
|
$applyNodeReplacement,
|
||||||
|
$createParagraphNode,
|
||||||
|
type DOMConversionMap,
|
||||||
|
DOMConversionOutput,
|
||||||
|
type DOMExportOutput,
|
||||||
|
type EditorConfig,
|
||||||
|
isHTMLElement,
|
||||||
|
type LexicalEditor,
|
||||||
|
type LexicalNode,
|
||||||
|
type NodeKey,
|
||||||
|
type ParagraphNode,
|
||||||
|
type RangeSelection,
|
||||||
|
type SerializedElementNode,
|
||||||
|
type Spread
|
||||||
|
} from "lexical";
|
||||||
|
import {addClassNamesToElement} from "@lexical/utils";
|
||||||
|
import {CommonBlockNode, copyCommonBlockProperties} from "lexical/nodes/CommonBlockNode";
|
||||||
|
import {
|
||||||
|
commonPropertiesDifferent, deserializeCommonBlockNode,
|
||||||
|
SerializedCommonBlockNode, setCommonBlockPropsFromElement,
|
||||||
|
updateElementWithCommonBlockProps
|
||||||
|
} from "../../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;
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
import {
|
||||||
|
$applyNodeReplacement,
|
||||||
|
$createParagraphNode,
|
||||||
|
type DOMConversionMap,
|
||||||
|
type DOMConversionOutput,
|
||||||
|
type DOMExportOutput,
|
||||||
|
type EditorConfig,
|
||||||
|
ElementNode,
|
||||||
|
isHTMLElement,
|
||||||
|
type LexicalEditor,
|
||||||
|
LexicalNode,
|
||||||
|
type NodeKey,
|
||||||
|
type ParagraphNode,
|
||||||
|
type RangeSelection,
|
||||||
|
SerializedElementNode
|
||||||
|
} from "lexical";
|
||||||
|
import {addClassNamesToElement} from "@lexical/utils";
|
||||||
|
import {CommonBlockNode, copyCommonBlockProperties} from "lexical/nodes/CommonBlockNode";
|
||||||
|
import {
|
||||||
|
commonPropertiesDifferent, deserializeCommonBlockNode,
|
||||||
|
SerializedCommonBlockNode, setCommonBlockPropsFromElement,
|
||||||
|
updateElementWithCommonBlockProps
|
||||||
|
} from "../../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,327 +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'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
element,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {
|
|
||||||
const node = $createQuoteNode();
|
|
||||||
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'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
element,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
|
|
||||||
return $createHeadingNode(serializedNode.tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
return {node};
|
|
||||||
}
|
|
||||||
|
|
||||||
function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
|
|
||||||
const node = $createQuoteNode();
|
|
||||||
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>,
|
||||||
|
|
|
@ -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[];
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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,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,4 +1,3 @@
|
||||||
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
|
|
||||||
import {CalloutNode} from './callout';
|
import {CalloutNode} from './callout';
|
||||||
import {
|
import {
|
||||||
ElementNode,
|
ElementNode,
|
||||||
|
@ -21,9 +20,9 @@ import {MediaNode} from "./media";
|
||||||
import {CustomListItemNode} from "./custom-list-item";
|
import {CustomListItemNode} from "./custom-list-item";
|
||||||
import {CustomTableCellNode} from "./custom-table-cell";
|
import {CustomTableCellNode} from "./custom-table-cell";
|
||||||
import {CustomTableRowNode} from "./custom-table-row";
|
import {CustomTableRowNode} from "./custom-table-row";
|
||||||
import {CustomHeadingNode} from "./custom-heading";
|
|
||||||
import {CustomQuoteNode} from "./custom-quote";
|
|
||||||
import {CustomListNode} from "./custom-list";
|
import {CustomListNode} from "./custom-list";
|
||||||
|
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
|
||||||
|
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the nodes for lexical.
|
* Load the nodes for lexical.
|
||||||
|
@ -31,8 +30,8 @@ import {CustomListNode} from "./custom-list";
|
||||||
export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {
|
export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {
|
||||||
return [
|
return [
|
||||||
CalloutNode,
|
CalloutNode,
|
||||||
CustomHeadingNode,
|
HeadingNode,
|
||||||
CustomQuoteNode,
|
QuoteNode,
|
||||||
CustomListNode,
|
CustomListNode,
|
||||||
CustomListItemNode, // TODO - Alignment?
|
CustomListItemNode, // TODO - Alignment?
|
||||||
CustomTableNode,
|
CustomTableNode,
|
||||||
|
@ -46,18 +45,6 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
|
||||||
MediaNode, // TODO - Alignment
|
MediaNode, // TODO - Alignment
|
||||||
ParagraphNode,
|
ParagraphNode,
|
||||||
LinkNode,
|
LinkNode,
|
||||||
{
|
|
||||||
replace: HeadingNode,
|
|
||||||
with: (node: HeadingNode) => {
|
|
||||||
return new CustomHeadingNode(node.__tag);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
replace: QuoteNode,
|
|
||||||
with: (node: QuoteNode) => {
|
|
||||||
return new CustomQuoteNode();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
replace: ListNode,
|
replace: ListNode,
|
||||||
with: (node: ListNode) => {
|
with: (node: ListNode) => {
|
||||||
|
|
|
@ -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,18 +2,14 @@ import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../
|
||||||
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 {
|
||||||
|
|
|
@ -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,4 +1,3 @@
|
||||||
import {$isQuoteNode, HeadingNode, HeadingTagType} from "@lexical/rich-text";
|
|
||||||
import {
|
import {
|
||||||
$createParagraphNode,
|
$createParagraphNode,
|
||||||
$createTextNode,
|
$createTextNode,
|
||||||
|
@ -15,23 +14,23 @@ import {
|
||||||
$toggleSelectionBlockNodeType,
|
$toggleSelectionBlockNodeType,
|
||||||
getLastSelection
|
getLastSelection
|
||||||
} from "./selection";
|
} from "./selection";
|
||||||
import {$createCustomHeadingNode, $isCustomHeadingNode} from "../nodes/custom-heading";
|
|
||||||
import {$createCustomQuoteNode} from "../nodes/custom-quote";
|
|
||||||
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../nodes/code-block";
|
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../nodes/code-block";
|
||||||
import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "../nodes/callout";
|
import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "../nodes/callout";
|
||||||
import {insertList, ListNode, ListType, removeList} from "@lexical/list";
|
import {insertList, ListNode, ListType, removeList} from "@lexical/list";
|
||||||
import {$isCustomListNode} from "../nodes/custom-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),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -44,7 +43,7 @@ export function toggleSelectionAsParagraph(editor: LexicalEditor) {
|
||||||
|
|
||||||
export function toggleSelectionAsBlockquote(editor: LexicalEditor) {
|
export function toggleSelectionAsBlockquote(editor: LexicalEditor) {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
$toggleSelectionBlockNodeType($isQuoteNode, $createCustomQuoteNode);
|
$toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue