diff --git a/packages/lexical-html/src/index.ts b/packages/lexical-html/src/index.ts index 3893b2062bb..8dbf1b416a2 100644 --- a/packages/lexical-html/src/index.ts +++ b/packages/lexical-html/src/index.ts @@ -92,7 +92,17 @@ function $appendNodesToHTML( target = clone; } const children = $isElementNode(target) ? target.getChildren() : []; - const {element, after} = target.exportDOM(editor); + const registeredNode = editor._nodes.get(target.getType()); + let exportOutput; + + // Use HTMLConfig overrides, if available. + if (registeredNode && registeredNode.exportDOM !== undefined) { + exportOutput = registeredNode.exportDOM(editor, target); + } else { + exportOutput = target.exportDOM(editor); + } + + const {element, after} = exportOutput; if (!element) { return false; diff --git a/packages/lexical-react/flow/LexicalComposer.js.flow b/packages/lexical-react/flow/LexicalComposer.js.flow index 8c32e5cb133..b4cb35ce9b4 100644 --- a/packages/lexical-react/flow/LexicalComposer.js.flow +++ b/packages/lexical-react/flow/LexicalComposer.js.flow @@ -13,6 +13,7 @@ import type { LexicalNode, EditorState, LexicalNodeReplacement, + HTMLConfig, } from 'lexical'; export type InitialEditorStateType = @@ -29,6 +30,7 @@ export type InitialConfigType = $ReadOnly<{ theme?: EditorThemeClasses, editorState?: InitialEditorStateType, onError: (error: Error, editor: LexicalEditor) => void, + html?: HTMLConfig, }>; type Props = { diff --git a/packages/lexical-react/src/LexicalComposer.tsx b/packages/lexical-react/src/LexicalComposer.tsx index be535c00930..8dc7ebc11d5 100644 --- a/packages/lexical-react/src/LexicalComposer.tsx +++ b/packages/lexical-react/src/LexicalComposer.tsx @@ -19,6 +19,7 @@ import { createEditor, EditorState, EditorThemeClasses, + HTMLConfig, Klass, LexicalEditor, LexicalNode, @@ -45,6 +46,7 @@ export type InitialConfigType = Readonly<{ editable?: boolean; theme?: EditorThemeClasses; editorState?: InitialEditorStateType; + html?: HTMLConfig; }>; type Props = { @@ -62,6 +64,7 @@ export function LexicalComposer({initialConfig, children}: Props): JSX.Element { nodes, onError, editorState: initialEditorState, + html, } = initialConfig; const context: LexicalComposerContextType = createLexicalComposerContext( @@ -74,6 +77,7 @@ export function LexicalComposer({initialConfig, children}: Props): JSX.Element { if (editor === null) { const newEditor = createEditor({ editable: initialConfig.editable, + html, namespace, nodes, onError: (error) => onError(error, newEditor), diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow index cb7760fe571..231114d00cc 100644 --- a/packages/lexical/flow/Lexical.js.flow +++ b/packages/lexical/flow/Lexical.js.flow @@ -278,6 +278,14 @@ export type LexicalNodeReplacement = { withKlass?: Class, }; +export type HTMLConfig = { + export?: Map< + Class, + (editor: LexicalEditor, target: LexicalNode) => DOMExportOutput, + >, + import?: DOMConversionMap, +}; + declare export function createEditor(editorConfig?: { editorState?: EditorState, namespace: string, @@ -287,6 +295,7 @@ declare export function createEditor(editorConfig?: { onError: (error: Error) => void, disableEvents?: boolean, editable?: boolean, + html?: HTMLConfig, }): LexicalEditor; /** diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index b2b5c1e1d01..df8600e2693 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -7,7 +7,12 @@ */ import type {EditorState, SerializedEditorState} from './LexicalEditorState'; -import type {DOMConversion, NodeKey} from './LexicalNode'; +import type { + DOMConversion, + DOMConversionMap, + DOMExportOutput, + NodeKey, +} from './LexicalNode'; import invariant from 'shared/invariant'; @@ -151,6 +156,14 @@ export type LexicalNodeReplacement = { withKlass?: Klass; }; +export type HTMLConfig = { + export?: Map< + Klass, + (editor: LexicalEditor, target: LexicalNode) => DOMExportOutput + >; + import?: DOMConversionMap; +}; + export type CreateEditorArgs = { disableEvents?: boolean; editorState?: EditorState; @@ -160,6 +173,7 @@ export type CreateEditorArgs = { parentEditor?: LexicalEditor; editable?: boolean; theme?: EditorThemeClasses; + html?: HTMLConfig; }; export type RegisteredNodes = Map; @@ -169,6 +183,10 @@ export type RegisteredNode = { transforms: Set>; replace: null | ((node: LexicalNode) => LexicalNode); replaceWithKlass: null | Klass; + exportDOM?: ( + editor: LexicalEditor, + targetNode: LexicalNode, + ) => DOMExportOutput; }; export type Transform = (node: T) => void; @@ -330,9 +348,24 @@ export function resetEditor( } } -function initializeConversionCache(nodes: RegisteredNodes): DOMConversionCache { +function initializeConversionCache( + nodes: RegisteredNodes, + additionalConversions?: DOMConversionMap, +): DOMConversionCache { const conversionCache = new Map(); const handledConversions = new Set(); + const addConversionsToCache = (map: DOMConversionMap) => { + Object.keys(map).forEach((key) => { + let currentCache = conversionCache.get(key); + + if (currentCache === undefined) { + currentCache = []; + conversionCache.set(key, currentCache); + } + + currentCache.push(map[key]); + }); + }; nodes.forEach((node) => { const importDOM = node.klass.importDOM != null @@ -347,18 +380,12 @@ function initializeConversionCache(nodes: RegisteredNodes): DOMConversionCache { const map = importDOM(); if (map !== null) { - Object.keys(map).forEach((key) => { - let currentCache = conversionCache.get(key); - - if (currentCache === undefined) { - currentCache = []; - conversionCache.set(key, currentCache); - } - - currentCache.push(map[key]); - }); + addConversionsToCache(map); } }); + if (additionalConversions) { + addConversionsToCache(additionalConversions); + } return conversionCache; } @@ -389,9 +416,9 @@ export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor { ParagraphNode, ...(config.nodes || []), ]; - const onError = config.onError; + const {onError, html} = config; const isEditable = config.editable !== undefined ? config.editable : true; - let registeredNodes; + let registeredNodes: Map; if (editorConfig === undefined && activeEditor !== null) { registeredNodes = activeEditor._nodes; @@ -457,11 +484,12 @@ export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor { } const type = klass.getType(); const transform = klass.transform(); - const transforms = new Set(); + const transforms = new Set>(); if (transform !== null) { transforms.add(transform); } registeredNodes.set(type, { + exportDOM: html && html.export ? html.export.get(klass) : undefined, klass, replace, replaceWithKlass, @@ -469,7 +497,6 @@ export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor { }); } } - const editor = new LexicalEditor( editorState, parentEditor, @@ -480,7 +507,7 @@ export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor { theme, }, onError ? onError : console.error, - initializeConversionCache(registeredNodes), + initializeConversionCache(registeredNodes, html ? html.import : undefined), isEditable, ); diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index a90cd519a8d..f7c61796515 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -16,6 +16,7 @@ export type { EditableListener, EditorConfig, EditorThemeClasses, + HTMLConfig, Klass, LexicalCommand, LexicalEditor,