Lexical: Extracted & merged heading & quote nodes

This commit is contained in:
Dan Brown 2024-12-03 17:04:50 +00:00
parent f3fa63a5ae
commit 36a4d79120
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
20 changed files with 370 additions and 651 deletions

View File

@ -8,11 +8,12 @@
import {$createLinkNode} from '@lexical/link';
import {$createListItemNode, $createListNode} from '@lexical/list';
import {$createHeadingNode, $createQuoteNode} from '@lexical/rich-text';
import {$createTableNodeWithDimensions} from '@lexical/table';
import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';
import {initializeUnitTest} from '../utils';
import {$createHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
import {$createQuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
function $createEditorContent() {
const root = $getRoot();

View File

@ -10,7 +10,6 @@ import {createHeadlessEditor} from '@lexical/headless';
import {AutoLinkNode, LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
import {
@ -36,6 +35,8 @@ import {
LexicalNodeReplacement,
} from '../../LexicalEditor';
import {resetRandomKey} from '../../LexicalUtils';
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
type TestEnv = {

View File

@ -48,7 +48,7 @@ export class CommonBlockNode extends ElementNode {
}
export function copyCommonBlockProperties(from: CommonBlockNode, to: CommonBlockNode): void {
to.__id = from.__id;
// to.__id = from.__id;
to.__alignment = from.__alignment;
to.__inset = from.__inset;
}

View File

@ -11,7 +11,7 @@ import {
$insertDataTransferForRichText,
} from '@lexical/clipboard';
import {$createListItemNode, $createListNode} from '@lexical/list';
import {$createHeadingNode, registerRichText} from '@lexical/rich-text';
import {registerRichText} from '@lexical/rich-text';
import {
$createParagraphNode,
$createRangeSelection,
@ -32,6 +32,7 @@ import {
initializeUnitTest,
invariant,
} from '../../../__tests__/utils';
import {$createHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
describe('LexicalTabNode tests', () => {
initializeUnitTest((testEnv) => {

View File

@ -13,13 +13,14 @@ import {createHeadlessEditor} from '@lexical/headless';
import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
import {LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {
$createParagraphNode,
$createRangeSelection,
$createTextNode,
$getRoot,
} from 'lexical';
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
describe('HTML', () => {
type Input = Array<{

View File

@ -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;
}

View File

@ -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};
}

View File

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

View File

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

View File

@ -8,42 +8,14 @@
import type {
CommandPayloadType,
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
EditorConfig,
ElementFormatType,
LexicalCommand,
LexicalEditor,
LexicalNode,
NodeKey,
ParagraphNode,
PasteCommandType,
RangeSelection,
SerializedElementNode,
Spread,
TextFormatType,
} from 'lexical';
import {
$insertDataTransferForRichText,
copyToClipboard,
} from '@lexical/clipboard';
import {
$moveCharacter,
$shouldOverrideDefaultCharacterSelection,
} from '@lexical/selection';
import {
$findMatchingParent,
$getNearestBlockElementAncestorOrThrow,
addClassNamesToElement,
isHTMLElement,
mergeRegister,
objectKlassEquals,
} from '@lexical/utils';
import {
$applyNodeReplacement,
$createParagraphNode,
$createRangeSelection,
$createTabNode,
$getAdjacentNode,
@ -55,7 +27,6 @@ import {
$isElementNode,
$isNodeSelection,
$isRangeSelection,
$isRootNode,
$isTextNode,
$normalizeSelection__EXPERIMENTAL,
$selectAll,
@ -75,7 +46,6 @@ import {
ElementNode,
FORMAT_ELEMENT_COMMAND,
FORMAT_TEXT_COMMAND,
INDENT_CONTENT_COMMAND,
INSERT_LINE_BREAK_COMMAND,
INSERT_PARAGRAPH_COMMAND,
INSERT_TAB_COMMAND,
@ -88,327 +58,22 @@ import {
KEY_DELETE_COMMAND,
KEY_ENTER_COMMAND,
KEY_ESCAPE_COMMAND,
OUTDENT_CONTENT_COMMAND,
PASTE_COMMAND,
REMOVE_TEXT_COMMAND,
SELECT_ALL_COMMAND,
} from 'lexical';
import caretFromPoint from 'lexical/shared/caretFromPoint';
import {
CAN_USE_BEFORE_INPUT,
IS_APPLE_WEBKIT,
IS_IOS,
IS_SAFARI,
} from 'lexical/shared/environment';
export type SerializedHeadingNode = Spread<
{
tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
},
SerializedElementNode
>;
import {$insertDataTransferForRichText, copyToClipboard,} from '@lexical/clipboard';
import {$moveCharacter, $shouldOverrideDefaultCharacterSelection,} from '@lexical/selection';
import {$findMatchingParent, mergeRegister, objectKlassEquals,} from '@lexical/utils';
import caretFromPoint from 'lexical/shared/caretFromPoint';
import {CAN_USE_BEFORE_INPUT, IS_APPLE_WEBKIT, IS_IOS, IS_SAFARI,} from 'lexical/shared/environment';
export const DRAG_DROP_PASTE: LexicalCommand<Array<File>> = createCommand(
'DRAG_DROP_PASTE_FILE',
);
export type SerializedQuoteNode = SerializedElementNode;
/** @noInheritDoc */
export class QuoteNode extends ElementNode {
static getType(): string {
return 'quote';
}
static clone(node: QuoteNode): QuoteNode {
return new QuoteNode(node.__key);
}
constructor(key?: NodeKey) {
super(key);
}
// View
createDOM(config: EditorConfig): HTMLElement {
const element = document.createElement('blockquote');
addClassNamesToElement(element, config.theme.quote);
return element;
}
updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean {
return false;
}
static importDOM(): DOMConversionMap | null {
return {
blockquote: (node: Node) => ({
conversion: $convertBlockquoteElement,
priority: 0,
}),
};
}
exportDOM(editor: LexicalEditor): DOMExportOutput {
const {element} = super.exportDOM(editor);
if (element && isHTMLElement(element)) {
if (this.isEmpty()) {
element.append(document.createElement('br'));
}
}
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(
event: CommandPayloadType<typeof PASTE_COMMAND>,

View File

@ -8,7 +8,7 @@
import {$createLinkNode} from '@lexical/link';
import {$createListItemNode, $createListNode} from '@lexical/list';
import {$createHeadingNode, registerRichText} from '@lexical/rich-text';
import {registerRichText} from '@lexical/rich-text';
import {
$addNodeStyle,
$getSelectionStyleValueForProperty,
@ -74,6 +74,7 @@ import {
} from '../utils';
import {createEmptyHistoryState, registerHistory} from "@lexical/history";
import {mergeRegister} from "@lexical/utils";
import {$createHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
interface ExpectedSelection {
anchorPath: number[];

View File

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

View File

@ -7,7 +7,7 @@
*/
import {AutoLinkNode, LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list';
import {HeadingNode, QuoteNode, registerRichText} from '@lexical/rich-text';
import {registerRichText} from '@lexical/rich-text';
import {
applySelectionInputs,
pasteHTML,
@ -15,6 +15,8 @@ import {
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
import {$createParagraphNode, $insertNodes, LexicalEditor} from 'lexical';
import {createTestEditor, initializeClipboard} from 'lexical/__tests__/utils';
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
jest.mock('lexical/shared/environment', () => {
const originalModule = jest.requireActual('lexical/shared/environment');

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {CalloutNode} from './callout';
import {
ElementNode,
@ -21,9 +20,9 @@ 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";
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
/**
* Load the nodes for lexical.
@ -31,8 +30,8 @@ import {CustomListNode} from "./custom-list";
export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {
return [
CalloutNode,
CustomHeadingNode,
CustomQuoteNode,
HeadingNode,
QuoteNode,
CustomListNode,
CustomListItemNode, // TODO - Alignment?
CustomTableNode,
@ -46,18 +45,6 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
MediaNode, // TODO - Alignment
ParagraphNode,
LinkNode,
{
replace: HeadingNode,
with: (node: HeadingNode) => {
return new CustomHeadingNode(node.__tag);
}
},
{
replace: QuoteNode,
with: (node: QuoteNode) => {
return new CustomQuoteNode();
}
},
{
replace: ListNode,
with: (node: ListNode) => {

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import {$isQuoteNode, HeadingNode, HeadingTagType} from "@lexical/rich-text";
import {
$createParagraphNode,
$createTextNode,
@ -15,23 +14,23 @@ import {
$toggleSelectionBlockNodeType,
getLastSelection
} 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 {$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 {$createHeadingNode, $isHeadingNode, HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode";
import {$createQuoteNode, $isQuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
const $isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
return $isCustomHeadingNode(node) && (node as HeadingNode).getTag() === tag;
return $isHeadingNode(node) && node.getTag() === tag;
};
export function toggleSelectionAsHeading(editor: LexicalEditor, tag: HeadingTagType) {
editor.update(() => {
$toggleSelectionBlockNodeType(
(node) => $isHeaderNodeOfTag(node, tag),
() => $createCustomHeadingNode(tag),
() => $createHeadingNode(tag),
)
});
}
@ -44,7 +43,7 @@ export function toggleSelectionAsParagraph(editor: LexicalEditor) {
export function toggleSelectionAsBlockquote(editor: LexicalEditor) {
editor.update(() => {
$toggleSelectionBlockNodeType($isQuoteNode, $createCustomQuoteNode);
$toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
});
}