Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 3 additions & 14 deletions examples/node-replacement/src/nodes/CustomParagraphNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,11 @@
* LICENSE file in the root directory of this source tree.
*
*/
import {
$applyNodeReplacement,
EditorConfig,
ParagraphNode,
SerializedParagraphNode,
} from 'lexical';
import {$applyNodeReplacement, EditorConfig, ParagraphNode} from 'lexical';

export class CustomParagraphNode extends ParagraphNode {
static getType() {
return 'custom-paragraph';
}
static clone(node: CustomParagraphNode): CustomParagraphNode {
return new CustomParagraphNode(node.__key);
}
static importJSON(json: SerializedParagraphNode): CustomParagraphNode {
return $createCustomParagraphNode().updateFromJSON(json);
$config() {
return this.config('custom-paragraph', {extends: ParagraphNode});
}
createDOM(config: EditorConfig) {
const el = super.createDOM(config);
Expand Down
12 changes: 11 additions & 1 deletion examples/react-rich/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,19 @@ const getExtraStyles = (element: HTMLElement): string => {
const constructImportMap = (): DOMConversionMap => {
const importMap: DOMConversionMap = {};

// With the $config() protocol a node's static methods (including importDOM)
// are generated on first static access rather than declared up front, so read
// getType() to ensure TextNode.importDOM is populated before we wrap its
// importers here (this runs before any editor, and therefore registration,
// exists).
TextNode.getType();
const importDOMFn = TextNode.importDOM;

// Wrap all TextNode importers with a function that also imports
// the custom styles implemented by the playground
for (const [tag, fn] of Object.entries(TextNode.importDOM() || {})) {
for (const [tag, fn] of Object.entries(
importDOMFn ? importDOMFn() || {} : {},
)) {
importMap[tag] = importNode => {
const importer = fn(importNode);
if (!importer) {
Expand Down
4 changes: 2 additions & 2 deletions examples/vanilla-js-plugin/src/emoji-plugin/EmojiNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ const BASE_EMOJI_URI = new URL(`@emoji-datasource-facebook/`, import.meta.url)
export class EmojiNode extends TextNode {
__unifiedID: string;

static getType(): string {
return 'emoji';
$config() {
return this.config('emoji', {extends: TextNode});
}

static clone(node: EmojiNode): EmojiNode {
Expand Down
18 changes: 2 additions & 16 deletions packages/lexical-code-core/src/CodeHighlightNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,8 @@ export class CodeHighlightNode extends TextNode {
this.__highlightType = highlightType;
}

static getType(): string {
return 'code-highlight';
}

static clone(node: CodeHighlightNode): CodeHighlightNode {
return new CodeHighlightNode(
node.__text,
node.__highlightType || undefined,
node.__key,
);
$config() {
return this.config('code-highlight', {extends: TextNode});
}

afterCloneFrom(prevNode: this): void {
Expand Down Expand Up @@ -110,12 +102,6 @@ export class CodeHighlightNode extends TextNode {
return update;
}

static importJSON(
serializedNode: SerializedCodeHighlightNode,
): CodeHighlightNode {
return $createCodeHighlightNode().updateFromJSON(serializedNode);
}

updateFromJSON(
serializedNode: LexicalUpdateJSON<SerializedCodeHighlightNode>,
): this {
Expand Down
149 changes: 71 additions & 78 deletions packages/lexical-code-core/src/CodeNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import type {CodeExtension} from './CodeExtension';
import type {
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
EditorConfig,
Expand Down Expand Up @@ -88,15 +87,80 @@ export class CodeNode extends ElementNode {
/** @internal */
__isSyntaxHighlightSupported: boolean;

static getType(): string {
return 'code';
}
$config() {
return this.config('code', {
extends: ElementNode,
importDOM: {
// Typically <pre> is used for code blocks, and <code> for inline code styles
// but if it's a multi line <code> we'll create a block. Pass through to
// inline format handled by TextNode otherwise.
code: (node: Node) => {
const isMultiLine =
node.textContent != null &&
(/\r?\n/.test(node.textContent) || hasChildDOMNodeTag(node, 'BR'));

return isMultiLine
? {
conversion: $convertPreElement,
priority: 1,
}
: null;
},
div: () => ({
conversion: $convertDivElement,
priority: 1,
}),
pre: () => ({
conversion: $convertPreElement,
priority: 0,
}),
table: (node: Node) => {
const table = node;
// domNode is a <table> since we matched it by nodeName
if (isGitHubCodeTable(table as HTMLTableElement)) {
return {
conversion: $convertTableElement,
priority: 3,
};
}
return null;
},
td: (node: Node) => {
// element is a <td> since we matched it by nodeName
const td = node as HTMLTableCellElement;
const table: HTMLTableElement | null = td.closest('table');

if (isGitHubCodeCell(td) || (table && isGitHubCodeTable(table))) {
// Return a no-op if it's a table cell in a code table, but not a code line.
// Otherwise it'll fall back to the T
return {
conversion: convertCodeNoop,
priority: 3,
};
}

static clone(node: CodeNode): CodeNode {
return new CodeNode(node.__language, node.__key);
return null;
},
tr: (node: Node) => {
// element is a <tr> since we matched it by nodeName
const tr = node as HTMLTableCellElement;
const table: HTMLTableElement | null = tr.closest('table');
if (table && isGitHubCodeTable(table)) {
return {
conversion: convertCodeNoop,
priority: 3,
};
}
return null;
},
},
});
}

constructor(language?: string | null | undefined, key?: NodeKey) {
// `language` carries an explicit `undefined` default so the constructor
// reports zero required arguments and `$config` can synthesize the static
// `clone` from the no-argument constructor.
constructor(language: string | null | undefined = undefined, key?: NodeKey) {
super(key);
this.__language = language || undefined;
this.__isSyntaxHighlightSupported = false;
Expand Down Expand Up @@ -207,77 +271,6 @@ export class CodeNode extends ElementNode {
return {element};
}

static importDOM(): DOMConversionMap | null {
return {
// Typically <pre> is used for code blocks, and <code> for inline code styles
// but if it's a multi line <code> we'll create a block. Pass through to
// inline format handled by TextNode otherwise.
code: (node: Node) => {
const isMultiLine =
node.textContent != null &&
(/\r?\n/.test(node.textContent) || hasChildDOMNodeTag(node, 'BR'));

return isMultiLine
? {
conversion: $convertPreElement,
priority: 1,
}
: null;
},
div: () => ({
conversion: $convertDivElement,
priority: 1,
}),
pre: () => ({
conversion: $convertPreElement,
priority: 0,
}),
table: (node: Node) => {
const table = node;
// domNode is a <table> since we matched it by nodeName
if (isGitHubCodeTable(table as HTMLTableElement)) {
return {
conversion: $convertTableElement,
priority: 3,
};
}
return null;
},
td: (node: Node) => {
// element is a <td> since we matched it by nodeName
const td = node as HTMLTableCellElement;
const table: HTMLTableElement | null = td.closest('table');

if (isGitHubCodeCell(td) || (table && isGitHubCodeTable(table))) {
// Return a no-op if it's a table cell in a code table, but not a code line.
// Otherwise it'll fall back to the T
return {
conversion: convertCodeNoop,
priority: 3,
};
}

return null;
},
tr: (node: Node) => {
// element is a <tr> since we matched it by nodeName
const tr = node as HTMLTableCellElement;
const table: HTMLTableElement | null = tr.closest('table');
if (table && isGitHubCodeTable(table)) {
return {
conversion: convertCodeNoop,
priority: 3,
};
}
return null;
},
};
}

static importJSON(serializedNode: SerializedCodeNode): CodeNode {
return $createCodeNode().updateFromJSON(serializedNode);
}

updateFromJSON(serializedNode: LexicalUpdateJSON<SerializedCodeNode>): this {
return super
.updateFromJSON(serializedNode)
Expand Down
27 changes: 16 additions & 11 deletions packages/lexical-code-core/src/FlatStructureUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
RangeSelection,
SiblingCaret,
TabNode,
TextNode,
} from 'lexical';

import invariant from '@lexical/internal/invariant';
Expand All @@ -33,11 +34,15 @@ import {
$isCodeHighlightNode,
} from './CodeHighlightNode';

function $getLastMatchingCodeNode<D extends CaretDirection>(
anchor: CodeHighlightNode | TabNode | LineBreakNode,
direction: D,
): CodeHighlightNode | TabNode | LineBreakNode {
let matchingNode: CodeHighlightNode | TabNode | LineBreakNode = anchor;
// The anchor is generic (rather than the narrower
// `CodeHighlightNode | TabNode | LineBreakNode`) because callers may have only
// narrowed as far as TextNode; the matched siblings are always
// CodeHighlightNode/TabNode, and an unmatched anchor is returned unchanged.
function $getLastMatchingCodeNode<
T extends TextNode | LineBreakNode,
D extends CaretDirection,
>(anchor: T, direction: D): T | CodeHighlightNode | TabNode {
let matchingNode: T | CodeHighlightNode | TabNode = anchor;
for (
let caret: null | SiblingCaret<LexicalNode, D> = $getSiblingCaret(
anchor,
Expand All @@ -51,15 +56,15 @@ function $getLastMatchingCodeNode<D extends CaretDirection>(
return matchingNode;
}

export function $getFirstCodeNodeOfLine(
anchor: CodeHighlightNode | TabNode | LineBreakNode,
): CodeHighlightNode | TabNode | LineBreakNode {
export function $getFirstCodeNodeOfLine<T extends TextNode | LineBreakNode>(
anchor: T,
): T | CodeHighlightNode | TabNode {
return $getLastMatchingCodeNode(anchor, 'previous');
}

export function $getLastCodeNodeOfLine(
anchor: CodeHighlightNode | TabNode | LineBreakNode,
): CodeHighlightNode | TabNode | LineBreakNode {
export function $getLastCodeNodeOfLine<T extends TextNode | LineBreakNode>(
anchor: T,
): T | CodeHighlightNode | TabNode {
return $getLastMatchingCodeNode(anchor, 'next');
}

Expand Down
36 changes: 14 additions & 22 deletions packages/lexical-extension/src/HorizontalRuleExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/

import type {
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
EditorConfig,
Expand Down Expand Up @@ -49,27 +48,20 @@ export const INSERT_HORIZONTAL_RULE_COMMAND: LexicalCommand<void> =
createCommand('INSERT_HORIZONTAL_RULE_COMMAND');

export class HorizontalRuleNode extends DecoratorNode<unknown> {
static getType(): string {
return 'horizontalrule';
}

static clone(node: HorizontalRuleNode): HorizontalRuleNode {
return new HorizontalRuleNode(node.__key);
}

static importJSON(
serializedNode: SerializedHorizontalRuleNode,
): HorizontalRuleNode {
return $createHorizontalRuleNode().updateFromJSON(serializedNode);
}

static importDOM(): DOMConversionMap | null {
return {
hr: () => ({
conversion: $convertHorizontalRuleElement,
priority: 0,
}),
};
$config() {
// `extends` is intentionally left to the runtime default (the prototype
// parent) rather than declared explicitly: the deprecated
// `@lexical/react` HorizontalRuleNode subclasses this one and reuses the
// same 'horizontalrule' type, so both `$config()` overrides must infer a
// matching shape.
return this.config('horizontalrule', {
importDOM: {
hr: () => ({
conversion: $convertHorizontalRuleElement,
priority: 0,
}),
},
});
}

exportDOM(): DOMExportOutput {
Expand Down
14 changes: 3 additions & 11 deletions packages/lexical-hashtag/src/LexicalHashtagNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,15 @@
*
*/

import type {EditorConfig, LexicalNode, SerializedTextNode} from 'lexical';
import type {EditorConfig, LexicalNode} from 'lexical';

import {addClassNamesToElement} from '@lexical/utils';
import {$applyNodeReplacement, TextNode} from 'lexical';

/** @noInheritDoc */
export class HashtagNode extends TextNode {
static getType(): string {
return 'hashtag';
}

static clone(node: HashtagNode): HashtagNode {
return new HashtagNode(node.__text, node.__key);
$config() {
return this.config('hashtag', {extends: TextNode});
}

createDOM(config: EditorConfig): HTMLElement {
Expand All @@ -27,10 +23,6 @@ export class HashtagNode extends TextNode {
return element;
}

static importJSON(serializedNode: SerializedTextNode): HashtagNode {
return $createHashtagNode().updateFromJSON(serializedNode);
}

canInsertTextBefore(): boolean {
return false;
}
Expand Down
Loading
Loading