feat(richtext-lexical): add htmlToLexical helper (#11479)
This adds a new `convertHTMLToLexical` helper that makes converting HTML to Lexical easy
This commit is contained in:
@@ -338,8 +338,8 @@ export const UploadFeature: FeatureProviderProviderServer<
|
||||
|
||||
Lexical provides a seamless way to perform conversions between various other formats:
|
||||
|
||||
- HTML to Lexical (or, importing HTML into the lexical editor)
|
||||
- Markdown to Lexical (or, importing Markdown into the lexical editor)
|
||||
- HTML to Lexical
|
||||
- Markdown to Lexical
|
||||
- Lexical to Markdown
|
||||
|
||||
A headless editor can perform such conversions outside of the main editor instance. Follow this method to initiate a headless editor:
|
||||
@@ -455,46 +455,22 @@ export const MyCollection: CollectionConfig = {
|
||||
|
||||
## HTML => Lexical
|
||||
|
||||
Once you have your headless editor instance, you can use it to convert HTML to Lexical:
|
||||
If you have access to the Payload Config and the lexical editor config, you can convert HTML to the lexical editor state with the following:
|
||||
|
||||
```ts
|
||||
import { $generateNodesFromDOM } from '@payloadcms/richtext-lexical/lexical/html'
|
||||
import { $getRoot, $getSelection } from '@payloadcms/richtext-lexical/lexical'
|
||||
import { convertHTMLToLexical, editorConfigFactory } from '@payloadcms/richtext-lexical'
|
||||
// Make sure you have jsdom and @types/jsdom installed
|
||||
import { JSDOM } from 'jsdom'
|
||||
|
||||
headlessEditor.update(
|
||||
() => {
|
||||
// In a headless environment you can use a package such as JSDom to parse the HTML string.
|
||||
const dom = new JSDOM(htmlString)
|
||||
|
||||
// Once you have the DOM instance it's easy to generate LexicalNodes.
|
||||
const nodes = $generateNodesFromDOM(headlessEditor, dom.window.document)
|
||||
|
||||
// Select the root
|
||||
$getRoot().select()
|
||||
|
||||
// Insert them at a selection.
|
||||
const selection = $getSelection()
|
||||
selection.insertNodes(nodes)
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
|
||||
// Do this if you then want to get the editor JSON
|
||||
const editorJSON = headlessEditor.getEditorState().toJSON()
|
||||
const html = convertHTMLToLexical({
|
||||
editorConfig: await editorConfigFactory.default({
|
||||
config, // <= make sure you have access to your Payload Config
|
||||
}),
|
||||
html: '<p>text</p>',
|
||||
JSDOM, // pass the JSDOM import. As it's a relatively large package, richtext-lexical does not include it by default.
|
||||
})
|
||||
```
|
||||
|
||||
Functions prefixed with a `$` can only be run inside an `editor.update()` or `editorState.read()` callback.
|
||||
|
||||
This has been taken from the [lexical serialization & deserialization docs](https://lexical.dev/docs/concepts/serialization#html---lexical).
|
||||
|
||||
<Banner type="success">
|
||||
**Note:**
|
||||
|
||||
Using the `discrete: true` flag ensures instant updates to the editor state. If
|
||||
immediate reading of the updated state isn't necessary, you can omit the flag.
|
||||
</Banner>
|
||||
|
||||
## Markdown => Lexical
|
||||
|
||||
Convert markdown content to the Lexical editor format with the following:
|
||||
@@ -516,6 +492,17 @@ headlessEditor.update(
|
||||
const editorJSON = headlessEditor.getEditorState().toJSON()
|
||||
```
|
||||
|
||||
Functions prefixed with a `$` can only be run inside an `editor.update()` or `editorState.read()` callback.
|
||||
|
||||
This has been taken from the [lexical serialization & deserialization docs](https://lexical.dev/docs/concepts/serialization#html---lexical).
|
||||
|
||||
<Banner type="success">
|
||||
**Note:**
|
||||
|
||||
Using the `discrete: true` flag ensures instant updates to the editor state. If
|
||||
immediate reading of the updated state isn't necessary, you can omit the flag.
|
||||
</Banner>
|
||||
|
||||
## Lexical => Markdown
|
||||
|
||||
Export content from the Lexical editor into Markdown format using these steps:
|
||||
@@ -564,7 +551,6 @@ Here's the code for it:
|
||||
|
||||
```ts
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { $getRoot } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
const yourEditorState: SerializedEditorState // <= your current editor state here
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { createHeadlessEditor } from '@lexical/headless'
|
||||
import { $getRoot, $getSelection, type SerializedLexicalNode } from 'lexical'
|
||||
|
||||
import type { SanitizedServerEditorConfig } from '../../../lexical/config/types.js'
|
||||
import type { DefaultNodeTypes, TypedEditorState } from '../../../nodeTypes.js'
|
||||
|
||||
import {} from '../../../lexical/config/server/sanitize.js'
|
||||
import { getEnabledNodes } from '../../../lexical/nodes/index.js'
|
||||
import { $generateNodesFromDOM } from '../../../lexical-proxy/@lexical-html.js'
|
||||
|
||||
export const convertHTMLToLexical = <TNodeTypes extends SerializedLexicalNode = DefaultNodeTypes>({
|
||||
editorConfig,
|
||||
html,
|
||||
JSDOM,
|
||||
}: {
|
||||
editorConfig: SanitizedServerEditorConfig
|
||||
html: string
|
||||
JSDOM: new (html: string) => {
|
||||
window: {
|
||||
document: Document
|
||||
}
|
||||
}
|
||||
}): TypedEditorState<TNodeTypes> => {
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
nodes: getEnabledNodes({
|
||||
editorConfig,
|
||||
}),
|
||||
})
|
||||
|
||||
headlessEditor.update(
|
||||
() => {
|
||||
const dom = new JSDOM(html)
|
||||
|
||||
const nodes = $generateNodesFromDOM(headlessEditor, dom.window.document)
|
||||
|
||||
$getRoot().select()
|
||||
|
||||
const selection = $getSelection()
|
||||
if (selection === null) {
|
||||
throw new Error('Selection is null')
|
||||
}
|
||||
selection.insertNodes(nodes)
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
|
||||
const editorJSON = headlessEditor.getEditorState().toJSON()
|
||||
|
||||
return editorJSON as TypedEditorState<TNodeTypes>
|
||||
}
|
||||
@@ -886,48 +886,49 @@ export {
|
||||
HTMLConverterFeature,
|
||||
type HTMLConverterFeatureProps,
|
||||
} from './features/converters/html/index.js'
|
||||
export { convertHTMLToLexical } from './features/converters/htmlToLexical/index.js'
|
||||
export { TestRecorderFeature } from './features/debug/testRecorder/server/index.js'
|
||||
export { TreeViewFeature } from './features/debug/treeView/server/index.js'
|
||||
export { EXPERIMENTAL_TableFeature } from './features/experimental_table/server/index.js'
|
||||
export { BoldFeature } from './features/format/bold/feature.server.js'
|
||||
export { InlineCodeFeature } from './features/format/inlineCode/feature.server.js'
|
||||
export { ItalicFeature } from './features/format/italic/feature.server.js'
|
||||
|
||||
export { ItalicFeature } from './features/format/italic/feature.server.js'
|
||||
export { StrikethroughFeature } from './features/format/strikethrough/feature.server.js'
|
||||
export { SubscriptFeature } from './features/format/subscript/feature.server.js'
|
||||
export { SuperscriptFeature } from './features/format/superscript/feature.server.js'
|
||||
export { UnderlineFeature } from './features/format/underline/feature.server.js'
|
||||
export { HeadingFeature, type HeadingFeatureProps } from './features/heading/server/index.js'
|
||||
export { HorizontalRuleFeature } from './features/horizontalRule/server/index.js'
|
||||
|
||||
export { IndentFeature } from './features/indent/server/index.js'
|
||||
|
||||
export { AutoLinkNode } from './features/link/nodes/AutoLinkNode.js'
|
||||
|
||||
export { LinkNode } from './features/link/nodes/LinkNode.js'
|
||||
export type { LinkFields } from './features/link/nodes/types.js'
|
||||
export { LinkFeature, type LinkFeatureServerProps } from './features/link/server/index.js'
|
||||
export { ChecklistFeature } from './features/lists/checklist/server/index.js'
|
||||
export { OrderedListFeature } from './features/lists/orderedList/server/index.js'
|
||||
|
||||
export { UnorderedListFeature } from './features/lists/unorderedList/server/index.js'
|
||||
|
||||
export type {
|
||||
SlateNode,
|
||||
SlateNodeConverter,
|
||||
} from './features/migrations/slateToLexical/converter/types.js'
|
||||
|
||||
export { ParagraphFeature } from './features/paragraph/server/index.js'
|
||||
export {
|
||||
RelationshipFeature,
|
||||
type RelationshipFeatureProps,
|
||||
} from './features/relationship/server/index.js'
|
||||
|
||||
export {
|
||||
type RelationshipData,
|
||||
RelationshipServerNode,
|
||||
} from './features/relationship/server/nodes/RelationshipNode.js'
|
||||
|
||||
export { FixedToolbarFeature } from './features/toolbars/fixed/server/index.js'
|
||||
export { InlineToolbarFeature } from './features/toolbars/inline/server/index.js'
|
||||
|
||||
export { InlineToolbarFeature } from './features/toolbars/inline/server/index.js'
|
||||
export type { ToolbarGroup, ToolbarGroupItem } from './features/toolbars/types.js'
|
||||
export type {
|
||||
BaseClientFeatureProps,
|
||||
@@ -942,6 +943,7 @@ export type {
|
||||
SanitizedClientFeatures,
|
||||
SanitizedPlugin,
|
||||
} from './features/typesClient.js'
|
||||
|
||||
export type {
|
||||
AfterChangeNodeHook,
|
||||
AfterChangeNodeHookArgs,
|
||||
@@ -967,37 +969,36 @@ export type {
|
||||
export { createNode } from './features/typeUtilities.js' // Only useful in feature.server.ts
|
||||
|
||||
export { UploadFeature } from './features/upload/server/feature.server.js'
|
||||
|
||||
export type { UploadFeatureProps } from './features/upload/server/feature.server.js'
|
||||
export { type UploadData, UploadServerNode } from './features/upload/server/nodes/UploadNode.js'
|
||||
|
||||
export { type UploadData, UploadServerNode } from './features/upload/server/nodes/UploadNode.js'
|
||||
export type { EditorConfigContextType } from './lexical/config/client/EditorConfigProvider.js'
|
||||
|
||||
export {
|
||||
defaultEditorConfig,
|
||||
defaultEditorFeatures,
|
||||
defaultEditorLexicalConfig,
|
||||
} from './lexical/config/server/default.js'
|
||||
|
||||
export { loadFeatures, sortFeaturesForOptimalLoading } from './lexical/config/server/loader.js'
|
||||
|
||||
export {
|
||||
sanitizeServerEditorConfig,
|
||||
sanitizeServerFeatures,
|
||||
} from './lexical/config/server/sanitize.js'
|
||||
|
||||
export type {
|
||||
ClientEditorConfig,
|
||||
SanitizedClientEditorConfig,
|
||||
SanitizedServerEditorConfig,
|
||||
ServerEditorConfig,
|
||||
} from './lexical/config/types.js'
|
||||
export { getEnabledNodes, getEnabledNodesFromServerNodes } from './lexical/nodes/index.js'
|
||||
export type { AdapterProps }
|
||||
|
||||
export { getEnabledNodes, getEnabledNodesFromServerNodes } from './lexical/nodes/index.js'
|
||||
|
||||
export type {
|
||||
SlashMenuGroup,
|
||||
SlashMenuItem,
|
||||
} from './lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.js'
|
||||
|
||||
export {
|
||||
DETAIL_TYPE_TO_DETAIL,
|
||||
DOUBLE_LINE_BREAK,
|
||||
@@ -1012,26 +1013,26 @@ export {
|
||||
TEXT_TYPE_TO_FORMAT,
|
||||
TEXT_TYPE_TO_MODE,
|
||||
} from './lexical/utils/nodeFormat.js'
|
||||
|
||||
export { sanitizeUrl, validateUrl } from './lexical/utils/url.js'
|
||||
|
||||
export type * from './nodeTypes.js'
|
||||
|
||||
export { $convertFromMarkdownString } from './packages/@lexical/markdown/index.js'
|
||||
|
||||
export { defaultRichTextValue } from './populateGraphQL/defaultValue.js'
|
||||
export { populate } from './populateGraphQL/populate.js'
|
||||
|
||||
export type { LexicalEditorProps, LexicalFieldAdminProps, LexicalRichTextAdapter } from './types.js'
|
||||
|
||||
export { createServerFeature } from './utilities/createServerFeature.js'
|
||||
|
||||
export { editorConfigFactory } from './utilities/editorConfigFactory.js'
|
||||
export type { FieldsDrawerProps } from './utilities/fieldsDrawer/Drawer.js'
|
||||
export { extractPropsFromJSXPropsString } from './utilities/jsx/extractPropsFromJSXPropsString.js'
|
||||
|
||||
export {
|
||||
extractFrontmatter,
|
||||
frontmatterToObject,
|
||||
objectToFrontmatter,
|
||||
propsToJSXString,
|
||||
} from './utilities/jsx/jsx.js'
|
||||
|
||||
export { upgradeLexicalData } from './utilities/upgradeLexicalData/index.js'
|
||||
|
||||
Reference in New Issue
Block a user