feat(richtext-lexical): SlateToLexical migration feature
This commit is contained in:
@@ -16,17 +16,38 @@ export const RichTextCell: React.FC<
|
|||||||
const [preview, setPreview] = React.useState('Loading...')
|
const [preview, setPreview] = React.useState('Loading...')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data == null) {
|
let dataToUse = data
|
||||||
|
if (dataToUse == null) {
|
||||||
setPreview('')
|
setPreview('')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transform data through load hooks
|
||||||
|
if (editorConfig?.features?.hooks?.load?.length) {
|
||||||
|
editorConfig.features.hooks.load.forEach((hook) => {
|
||||||
|
dataToUse = hook({ incomingEditorState: dataToUse })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// If data is from Slate and not Lexical
|
||||||
|
if (dataToUse && Array.isArray(dataToUse) && !('root' in dataToUse)) {
|
||||||
|
setPreview('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If data is from payload-plugin-lexical
|
||||||
|
if (dataToUse && 'jsonContent' in dataToUse) {
|
||||||
|
setPreview('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// initialize headless editor
|
// initialize headless editor
|
||||||
const headlessEditor = createHeadlessEditor({
|
const headlessEditor = createHeadlessEditor({
|
||||||
namespace: editorConfig.lexical.namespace,
|
namespace: editorConfig.lexical.namespace,
|
||||||
nodes: getEnabledNodes({ editorConfig }),
|
nodes: getEnabledNodes({ editorConfig }),
|
||||||
theme: editorConfig.lexical.theme,
|
theme: editorConfig.lexical.theme,
|
||||||
})
|
})
|
||||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(data))
|
headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse))
|
||||||
|
|
||||||
const textContent =
|
const textContent =
|
||||||
headlessEditor.getEditorState().read(() => {
|
headlessEditor.getEditorState().read(() => {
|
||||||
|
|||||||
@@ -89,9 +89,16 @@ const RichText: React.FC<FieldProps> = (props) => {
|
|||||||
fieldProps={props}
|
fieldProps={props}
|
||||||
initialState={initialValue}
|
initialState={initialValue}
|
||||||
onChange={(editorState, editor, tags) => {
|
onChange={(editorState, editor, tags) => {
|
||||||
const json = editorState.toJSON()
|
let serializedEditorState = editorState.toJSON()
|
||||||
|
|
||||||
setValue(json)
|
// Transform state through save hooks
|
||||||
|
if (editorConfig?.features?.hooks?.save?.length) {
|
||||||
|
editorConfig.features.hooks.save.forEach((hook) => {
|
||||||
|
serializedEditorState = hook({ incomingEditorState: serializedEditorState })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(serializedEditorState)
|
||||||
}}
|
}}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { SerializedHeadingNode } from '@lexical/rich-text'
|
||||||
|
|
||||||
|
import type { SlateNodeConverter } from '../types'
|
||||||
|
|
||||||
|
import { convertSlateNodesToLexical } from '..'
|
||||||
|
|
||||||
|
export const HeadingConverter: SlateNodeConverter = {
|
||||||
|
converter({ converters, slateNode }) {
|
||||||
|
return {
|
||||||
|
children: convertSlateNodesToLexical({
|
||||||
|
canContainParagraphs: false,
|
||||||
|
converters,
|
||||||
|
parentNodeType: 'heading',
|
||||||
|
slateNodes: slateNode.children || [],
|
||||||
|
}),
|
||||||
|
direction: 'ltr',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
tag: slateNode.type as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6', // Slate puts the tag (h1 / h2 / ...) inside of node.type
|
||||||
|
type: 'heading',
|
||||||
|
version: 1,
|
||||||
|
} as const as SerializedHeadingNode
|
||||||
|
},
|
||||||
|
nodeTypes: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { SerializedLexicalNode, SerializedParagraphNode } from 'lexical'
|
||||||
|
|
||||||
|
import type { SlateNodeConverter } from '../types'
|
||||||
|
|
||||||
|
import { convertSlateNodesToLexical } from '..'
|
||||||
|
|
||||||
|
export const IndentConverter: SlateNodeConverter = {
|
||||||
|
converter({ converters, slateNode }) {
|
||||||
|
console.log('slateToLexical > IndentConverter > converter', JSON.stringify(slateNode, null, 2))
|
||||||
|
const convertChildren = (node: any, indentLevel: number = 0): SerializedLexicalNode => {
|
||||||
|
if (
|
||||||
|
(node?.type && (!node.children || node.type !== 'indent')) ||
|
||||||
|
(!node?.type && node?.text)
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
'slateToLexical > IndentConverter > convertChildren > node',
|
||||||
|
JSON.stringify(node, null, 2),
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
'slateToLexical > IndentConverter > convertChildren > nodeOutput',
|
||||||
|
JSON.stringify(
|
||||||
|
convertSlateNodesToLexical({
|
||||||
|
canContainParagraphs: false,
|
||||||
|
converters,
|
||||||
|
parentNodeType: 'indent',
|
||||||
|
slateNodes: [node],
|
||||||
|
}),
|
||||||
|
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...convertSlateNodesToLexical({
|
||||||
|
canContainParagraphs: false,
|
||||||
|
converters,
|
||||||
|
parentNodeType: 'indent',
|
||||||
|
slateNodes: [node],
|
||||||
|
})[0],
|
||||||
|
indent: indentLevel,
|
||||||
|
} as const as SerializedLexicalNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = node.children.map((child: any) => convertChildren(child, indentLevel + 1))
|
||||||
|
console.log('slateToLexical > IndentConverter > children', JSON.stringify(children, null, 2))
|
||||||
|
return {
|
||||||
|
children: children,
|
||||||
|
direction: 'ltr',
|
||||||
|
format: '',
|
||||||
|
indent: indentLevel,
|
||||||
|
type: 'paragraph',
|
||||||
|
version: 1,
|
||||||
|
} as const as SerializedParagraphNode
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'slateToLexical > IndentConverter > output',
|
||||||
|
JSON.stringify(convertChildren(slateNode), null, 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
return convertChildren(slateNode)
|
||||||
|
},
|
||||||
|
nodeTypes: ['indent'],
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { SerializedLinkNode } from '../../../../Link/nodes/LinkNode'
|
||||||
|
import type { SlateNodeConverter } from '../types'
|
||||||
|
|
||||||
|
import { convertSlateNodesToLexical } from '..'
|
||||||
|
|
||||||
|
export const LinkConverter: SlateNodeConverter = {
|
||||||
|
converter({ converters, slateNode }) {
|
||||||
|
return {
|
||||||
|
children: convertSlateNodesToLexical({
|
||||||
|
canContainParagraphs: false,
|
||||||
|
converters,
|
||||||
|
parentNodeType: 'link',
|
||||||
|
slateNodes: slateNode.children || [],
|
||||||
|
}),
|
||||||
|
direction: 'ltr',
|
||||||
|
fields: {
|
||||||
|
doc: slateNode.doc || undefined,
|
||||||
|
linkType: slateNode.linkType || 'custom',
|
||||||
|
newTab: slateNode.newTab || false,
|
||||||
|
url: slateNode.url || undefined,
|
||||||
|
},
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
type: 'link',
|
||||||
|
version: 1,
|
||||||
|
} as const as SerializedLinkNode
|
||||||
|
},
|
||||||
|
nodeTypes: ['link'],
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { SerializedListItemNode } from '@lexical/list'
|
||||||
|
|
||||||
|
import type { SlateNodeConverter } from '../types'
|
||||||
|
|
||||||
|
import { convertSlateNodesToLexical } from '..'
|
||||||
|
|
||||||
|
export const ListItemConverter: SlateNodeConverter = {
|
||||||
|
converter({ childIndex, converters, slateNode }) {
|
||||||
|
return {
|
||||||
|
checked: undefined,
|
||||||
|
children: convertSlateNodesToLexical({
|
||||||
|
canContainParagraphs: false,
|
||||||
|
converters,
|
||||||
|
parentNodeType: 'listitem',
|
||||||
|
slateNodes: slateNode.children || [],
|
||||||
|
}),
|
||||||
|
direction: 'ltr',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
type: 'listitem',
|
||||||
|
value: childIndex + 1,
|
||||||
|
version: 1,
|
||||||
|
} as const as SerializedListItemNode
|
||||||
|
},
|
||||||
|
nodeTypes: ['li'],
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { SerializedListNode } from '@lexical/list'
|
||||||
|
|
||||||
|
import type { SlateNodeConverter } from '../types'
|
||||||
|
|
||||||
|
import { convertSlateNodesToLexical } from '..'
|
||||||
|
|
||||||
|
export const OrderedListConverter: SlateNodeConverter = {
|
||||||
|
converter({ converters, slateNode }) {
|
||||||
|
return {
|
||||||
|
children: convertSlateNodesToLexical({
|
||||||
|
canContainParagraphs: false,
|
||||||
|
converters,
|
||||||
|
parentNodeType: 'list',
|
||||||
|
slateNodes: slateNode.children || [],
|
||||||
|
}),
|
||||||
|
direction: 'ltr',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
listType: 'number',
|
||||||
|
start: 1,
|
||||||
|
tag: 'ol',
|
||||||
|
type: 'list',
|
||||||
|
version: 1,
|
||||||
|
} as const as SerializedListNode
|
||||||
|
},
|
||||||
|
nodeTypes: ['ol'],
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { SerializedRelationshipNode } from '../../../../../..'
|
||||||
|
import type { SlateNodeConverter } from '../types'
|
||||||
|
|
||||||
|
export const RelationshipConverter: SlateNodeConverter = {
|
||||||
|
converter({ slateNode }) {
|
||||||
|
return {
|
||||||
|
format: '',
|
||||||
|
relationTo: slateNode.relationTo,
|
||||||
|
type: 'relationship',
|
||||||
|
value: {
|
||||||
|
id: slateNode?.value?.id || '',
|
||||||
|
},
|
||||||
|
version: 1,
|
||||||
|
} as const as SerializedRelationshipNode
|
||||||
|
},
|
||||||
|
nodeTypes: ['relationship'],
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { SerializedUnknownConvertedNode } from '../../nodes/unknownConvertedNode'
|
||||||
|
import type { SlateNodeConverter } from '../types'
|
||||||
|
|
||||||
|
import { convertSlateNodesToLexical } from '..'
|
||||||
|
|
||||||
|
export const UnknownConverter: SlateNodeConverter = {
|
||||||
|
converter({ converters, slateNode }) {
|
||||||
|
return {
|
||||||
|
children: convertSlateNodesToLexical({
|
||||||
|
canContainParagraphs: false,
|
||||||
|
converters,
|
||||||
|
parentNodeType: 'unknownConverted',
|
||||||
|
slateNodes: slateNode.children || [],
|
||||||
|
}),
|
||||||
|
data: {
|
||||||
|
nodeData: slateNode,
|
||||||
|
nodeType: slateNode.type,
|
||||||
|
},
|
||||||
|
direction: 'ltr',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
type: 'unknownConverted',
|
||||||
|
version: 1,
|
||||||
|
} as const as SerializedUnknownConvertedNode
|
||||||
|
},
|
||||||
|
nodeTypes: ['unknown'],
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { SerializedListNode } from '@lexical/list'
|
||||||
|
|
||||||
|
import type { SlateNodeConverter } from '../types'
|
||||||
|
|
||||||
|
import { convertSlateNodesToLexical } from '..'
|
||||||
|
|
||||||
|
export const UnorderedListConverter: SlateNodeConverter = {
|
||||||
|
converter({ converters, slateNode }) {
|
||||||
|
return {
|
||||||
|
children: convertSlateNodesToLexical({
|
||||||
|
canContainParagraphs: false,
|
||||||
|
converters,
|
||||||
|
parentNodeType: 'list',
|
||||||
|
slateNodes: slateNode.children || [],
|
||||||
|
}),
|
||||||
|
direction: 'ltr',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
listType: 'bullet',
|
||||||
|
start: 1,
|
||||||
|
tag: 'ul',
|
||||||
|
type: 'list',
|
||||||
|
version: 1,
|
||||||
|
} as const as SerializedListNode
|
||||||
|
},
|
||||||
|
nodeTypes: ['ul'],
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { SerializedUploadNode } from '../../../../../..'
|
||||||
|
import type { SlateNodeConverter } from '../types'
|
||||||
|
|
||||||
|
export const UploadConverter: SlateNodeConverter = {
|
||||||
|
converter({ slateNode }) {
|
||||||
|
return {
|
||||||
|
fields: {
|
||||||
|
...slateNode.fields,
|
||||||
|
},
|
||||||
|
format: '',
|
||||||
|
relationTo: slateNode.relationTo,
|
||||||
|
type: 'upload',
|
||||||
|
value: {
|
||||||
|
id: slateNode.value?.id || '',
|
||||||
|
},
|
||||||
|
version: 1,
|
||||||
|
} as const as SerializedUploadNode
|
||||||
|
},
|
||||||
|
nodeTypes: ['upload'],
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { SlateNodeConverter } from './types'
|
||||||
|
|
||||||
|
import { HeadingConverter } from './converters/heading'
|
||||||
|
import { IndentConverter } from './converters/indent'
|
||||||
|
import { LinkConverter } from './converters/link'
|
||||||
|
import { ListItemConverter } from './converters/listItem'
|
||||||
|
import { OrderedListConverter } from './converters/orderedList'
|
||||||
|
import { RelationshipConverter } from './converters/relationship'
|
||||||
|
import { UnknownConverter } from './converters/unknown'
|
||||||
|
import { UnorderedListConverter } from './converters/unorderedList'
|
||||||
|
import { UploadConverter } from './converters/upload'
|
||||||
|
|
||||||
|
export const defaultConverters: SlateNodeConverter[] = [
|
||||||
|
UnknownConverter,
|
||||||
|
UploadConverter,
|
||||||
|
UnorderedListConverter,
|
||||||
|
OrderedListConverter,
|
||||||
|
RelationshipConverter,
|
||||||
|
ListItemConverter,
|
||||||
|
LinkConverter,
|
||||||
|
HeadingConverter,
|
||||||
|
IndentConverter,
|
||||||
|
]
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import type {
|
||||||
|
SerializedEditorState,
|
||||||
|
SerializedLexicalNode,
|
||||||
|
SerializedParagraphNode,
|
||||||
|
SerializedTextNode,
|
||||||
|
} from 'lexical'
|
||||||
|
|
||||||
|
import type { SlateNode, SlateNodeConverter } from './types'
|
||||||
|
|
||||||
|
import { NodeFormat } from '../../../../lexical/utils/nodeFormat'
|
||||||
|
|
||||||
|
export function convertSlateToLexical({
|
||||||
|
converters,
|
||||||
|
slateData,
|
||||||
|
}: {
|
||||||
|
converters: SlateNodeConverter[]
|
||||||
|
slateData: SlateNode[]
|
||||||
|
}): SerializedEditorState {
|
||||||
|
return {
|
||||||
|
root: {
|
||||||
|
children: convertSlateNodesToLexical({
|
||||||
|
canContainParagraphs: true,
|
||||||
|
converters,
|
||||||
|
parentNodeType: 'root',
|
||||||
|
slateNodes: slateData,
|
||||||
|
}),
|
||||||
|
direction: 'ltr',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
type: 'root',
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertSlateNodesToLexical({
|
||||||
|
canContainParagraphs,
|
||||||
|
converters,
|
||||||
|
parentNodeType,
|
||||||
|
slateNodes,
|
||||||
|
}: {
|
||||||
|
canContainParagraphs: boolean
|
||||||
|
converters: SlateNodeConverter[]
|
||||||
|
/**
|
||||||
|
* Type of the parent lexical node (not the type of the original, parent slate type)
|
||||||
|
*/
|
||||||
|
parentNodeType: string
|
||||||
|
slateNodes: SlateNode[]
|
||||||
|
}): SerializedLexicalNode[] {
|
||||||
|
const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown'))
|
||||||
|
return (
|
||||||
|
slateNodes.map((slateNode, i) => {
|
||||||
|
if (!('type' in slateNode)) {
|
||||||
|
if (canContainParagraphs) {
|
||||||
|
// This is a paragraph node. They do not have a type property in Slate
|
||||||
|
return convertParagraphNode(converters, slateNode)
|
||||||
|
} else {
|
||||||
|
// This is a simple text node. canContainParagraphs may be false if this is nested inside of a paragraph already, since paragraphs cannot contain paragraphs
|
||||||
|
return convertTextNode(slateNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (slateNode.type === 'p') {
|
||||||
|
return convertParagraphNode(converters, slateNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
const converter = converters.find((converter) => converter.nodeTypes.includes(slateNode.type))
|
||||||
|
|
||||||
|
if (converter) {
|
||||||
|
return converter.converter({ childIndex: i, converters, parentNodeType, slateNode })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('slateToLexical > No converter found for node type: ' + slateNode.type)
|
||||||
|
return unknownConverter?.converter({
|
||||||
|
childIndex: i,
|
||||||
|
converters,
|
||||||
|
parentNodeType,
|
||||||
|
slateNode,
|
||||||
|
})
|
||||||
|
}) || []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertParagraphNode(
|
||||||
|
converters: SlateNodeConverter[],
|
||||||
|
node: SlateNode,
|
||||||
|
): SerializedParagraphNode {
|
||||||
|
return {
|
||||||
|
children: convertSlateNodesToLexical({
|
||||||
|
canContainParagraphs: false,
|
||||||
|
converters,
|
||||||
|
parentNodeType: 'paragraph',
|
||||||
|
slateNodes: node.children || [],
|
||||||
|
}),
|
||||||
|
direction: 'ltr',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
type: 'paragraph',
|
||||||
|
version: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function convertTextNode(node: SlateNode): SerializedTextNode {
|
||||||
|
return {
|
||||||
|
detail: 0,
|
||||||
|
format: convertNodeToFormat(node),
|
||||||
|
mode: 'normal',
|
||||||
|
style: '',
|
||||||
|
text: node.text,
|
||||||
|
type: 'text',
|
||||||
|
version: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertNodeToFormat(node: SlateNode): number {
|
||||||
|
let format = 0
|
||||||
|
if (node.bold) {
|
||||||
|
format = format | NodeFormat.IS_BOLD
|
||||||
|
}
|
||||||
|
if (node.italic) {
|
||||||
|
format = format | NodeFormat.IS_ITALIC
|
||||||
|
}
|
||||||
|
if (node.strikethrough) {
|
||||||
|
format = format | NodeFormat.IS_STRIKETHROUGH
|
||||||
|
}
|
||||||
|
if (node.underline) {
|
||||||
|
format = format | NodeFormat.IS_UNDERLINE
|
||||||
|
}
|
||||||
|
if (node.subscript) {
|
||||||
|
format = format | NodeFormat.IS_SUBSCRIPT
|
||||||
|
}
|
||||||
|
if (node.superscript) {
|
||||||
|
format = format | NodeFormat.IS_SUPERSCRIPT
|
||||||
|
}
|
||||||
|
if (node.code) {
|
||||||
|
format = format | NodeFormat.IS_CODE
|
||||||
|
}
|
||||||
|
return format
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { SerializedLexicalNode } from 'lexical'
|
||||||
|
|
||||||
|
export type SlateNodeConverter<T extends SerializedLexicalNode = SerializedLexicalNode> = {
|
||||||
|
converter: ({
|
||||||
|
childIndex,
|
||||||
|
converters,
|
||||||
|
parentNodeType,
|
||||||
|
slateNode,
|
||||||
|
}: {
|
||||||
|
childIndex: number
|
||||||
|
converters: SlateNodeConverter[]
|
||||||
|
parentNodeType: string
|
||||||
|
slateNode: SlateNode
|
||||||
|
}) => T
|
||||||
|
nodeTypes: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SlateNode = {
|
||||||
|
[key: string]: any
|
||||||
|
children?: SlateNode[]
|
||||||
|
type?: string // doesn't always have type, e.g. for paragraphs
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { FeatureProvider } from '../../types'
|
||||||
|
import type { SlateNodeConverter } from './converter/types'
|
||||||
|
|
||||||
|
import { convertSlateToLexical } from './converter'
|
||||||
|
import { defaultConverters } from './converter/defaultConverters'
|
||||||
|
import { UnknownConvertedNode } from './nodes/unknownConvertedNode'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
converters?:
|
||||||
|
| (({ defaultConverters }: { defaultConverters: SlateNodeConverter[] }) => SlateNodeConverter[])
|
||||||
|
| SlateNodeConverter[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SlateToLexicalFeature = (props?: Props): FeatureProvider => {
|
||||||
|
if (!props) {
|
||||||
|
props = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
props.converters =
|
||||||
|
props?.converters && typeof props?.converters === 'function'
|
||||||
|
? props.converters({ defaultConverters: defaultConverters })
|
||||||
|
: (props?.converters as SlateNodeConverter[]) || defaultConverters
|
||||||
|
|
||||||
|
return {
|
||||||
|
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||||
|
return {
|
||||||
|
hooks: {
|
||||||
|
load({ incomingEditorState }) {
|
||||||
|
if (
|
||||||
|
!incomingEditorState ||
|
||||||
|
!Array.isArray(incomingEditorState) ||
|
||||||
|
'root' in incomingEditorState
|
||||||
|
) {
|
||||||
|
// incomingEditorState null or not from Slate
|
||||||
|
return incomingEditorState
|
||||||
|
}
|
||||||
|
// Slate => convert to lexical
|
||||||
|
|
||||||
|
return convertSlateToLexical({
|
||||||
|
converters: props.converters as SlateNodeConverter[],
|
||||||
|
slateData: incomingEditorState,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
node: UnknownConvertedNode,
|
||||||
|
type: UnknownConvertedNode.getType(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
props,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key: 'slateToLexical',
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
@import 'payload/scss';
|
||||||
|
|
||||||
|
span.unknownConverted {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
font-size: base(0.5);
|
||||||
|
margin: 0 0 base(1);
|
||||||
|
background: red;
|
||||||
|
color: white;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
div {
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import type { SerializedLexicalNode, Spread } from 'lexical'
|
||||||
|
|
||||||
|
import { addClassNamesToElement } from '@lexical/utils'
|
||||||
|
import { DecoratorNode, type EditorConfig, type LexicalNode, type NodeKey } from 'lexical'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
export type UnknownConvertedNodeData = {
|
||||||
|
nodeData: unknown
|
||||||
|
nodeType: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SerializedUnknownConvertedNode = Spread<
|
||||||
|
{
|
||||||
|
data: UnknownConvertedNodeData
|
||||||
|
},
|
||||||
|
SerializedLexicalNode
|
||||||
|
>
|
||||||
|
|
||||||
|
/** @noInheritDoc */
|
||||||
|
export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
|
||||||
|
__data: UnknownConvertedNodeData
|
||||||
|
|
||||||
|
constructor({ data, key }: { data: UnknownConvertedNodeData; key?: NodeKey }) {
|
||||||
|
super(key)
|
||||||
|
this.__data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
static clone(node: UnknownConvertedNode): UnknownConvertedNode {
|
||||||
|
return new UnknownConvertedNode({
|
||||||
|
data: node.__data,
|
||||||
|
key: node.__key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static getType(): string {
|
||||||
|
return 'unknownConverted'
|
||||||
|
}
|
||||||
|
|
||||||
|
static importJSON(serializedNode: SerializedUnknownConvertedNode): UnknownConvertedNode {
|
||||||
|
const node = $createUnknownConvertedNode({ data: serializedNode.data })
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
canInsertTextAfter(): true {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
canInsertTextBefore(): true {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
createDOM(config: EditorConfig): HTMLElement {
|
||||||
|
const element = document.createElement('span')
|
||||||
|
addClassNamesToElement(element, 'unknownConverted')
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
|
||||||
|
decorate(): JSX.Element | null {
|
||||||
|
return <div>Unknown converted Slate node: {this.__data?.nodeType}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
exportJSON(): SerializedUnknownConvertedNode {
|
||||||
|
return {
|
||||||
|
data: this.__data,
|
||||||
|
type: this.getType(),
|
||||||
|
version: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutation
|
||||||
|
|
||||||
|
isInline(): boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDOM(prevNode: UnknownConvertedNode, dom: HTMLElement): boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $createUnknownConvertedNode({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: UnknownConvertedNodeData
|
||||||
|
}): UnknownConvertedNode {
|
||||||
|
return new UnknownConvertedNode({
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $isUnknownConvertedNode(
|
||||||
|
node: LexicalNode | null | undefined,
|
||||||
|
): node is UnknownConvertedNode {
|
||||||
|
return node instanceof UnknownConvertedNode
|
||||||
|
}
|
||||||
@@ -51,6 +51,18 @@ export type Feature = {
|
|||||||
floatingSelectToolbar?: {
|
floatingSelectToolbar?: {
|
||||||
sections: FloatingToolbarSection[]
|
sections: FloatingToolbarSection[]
|
||||||
}
|
}
|
||||||
|
hooks?: {
|
||||||
|
load?: ({
|
||||||
|
incomingEditorState,
|
||||||
|
}: {
|
||||||
|
incomingEditorState: SerializedEditorState
|
||||||
|
}) => SerializedEditorState
|
||||||
|
save?: ({
|
||||||
|
incomingEditorState,
|
||||||
|
}: {
|
||||||
|
incomingEditorState: SerializedEditorState
|
||||||
|
}) => SerializedEditorState
|
||||||
|
}
|
||||||
markdownTransformers?: Transformer[]
|
markdownTransformers?: Transformer[]
|
||||||
nodes?: Array<{
|
nodes?: Array<{
|
||||||
afterReadPromises?: Array<AfterReadPromise>
|
afterReadPromises?: Array<AfterReadPromise>
|
||||||
@@ -123,6 +135,22 @@ export type SanitizedFeatures = Required<
|
|||||||
floatingSelectToolbar: {
|
floatingSelectToolbar: {
|
||||||
sections: FloatingToolbarSection[]
|
sections: FloatingToolbarSection[]
|
||||||
}
|
}
|
||||||
|
hooks: {
|
||||||
|
load: Array<
|
||||||
|
({
|
||||||
|
incomingEditorState,
|
||||||
|
}: {
|
||||||
|
incomingEditorState: SerializedEditorState
|
||||||
|
}) => SerializedEditorState
|
||||||
|
>
|
||||||
|
save: Array<
|
||||||
|
({
|
||||||
|
incomingEditorState,
|
||||||
|
}: {
|
||||||
|
incomingEditorState: SerializedEditorState
|
||||||
|
}) => SerializedEditorState
|
||||||
|
>
|
||||||
|
}
|
||||||
plugins?: Array<
|
plugins?: Array<
|
||||||
| {
|
| {
|
||||||
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
||||||
|
|||||||
@@ -21,7 +21,16 @@ export type LexicalProviderProps = {
|
|||||||
value: SerializedEditorState
|
value: SerializedEditorState
|
||||||
}
|
}
|
||||||
export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
|
export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
|
||||||
const { editorConfig, fieldProps, initialState, onChange, readOnly, setValue, value } = props
|
const { editorConfig, fieldProps, onChange, readOnly, setValue } = props
|
||||||
|
let { initialState, value } = props
|
||||||
|
|
||||||
|
// Transform initialState through load hooks
|
||||||
|
if (editorConfig?.features?.hooks?.load?.length) {
|
||||||
|
editorConfig.features.hooks.load.forEach((hook) => {
|
||||||
|
initialState = hook({ incomingEditorState: initialState })
|
||||||
|
value = hook({ incomingEditorState: value })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(value && Array.isArray(value) && !('root' in value)) ||
|
(value && Array.isArray(value) && !('root' in value)) ||
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
|||||||
floatingSelectToolbar: {
|
floatingSelectToolbar: {
|
||||||
sections: [],
|
sections: [],
|
||||||
},
|
},
|
||||||
|
hooks: {
|
||||||
|
load: [],
|
||||||
|
save: [],
|
||||||
|
},
|
||||||
markdownTransformers: [],
|
markdownTransformers: [],
|
||||||
nodes: [],
|
nodes: [],
|
||||||
plugins: [],
|
plugins: [],
|
||||||
@@ -21,6 +25,15 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
|||||||
}
|
}
|
||||||
|
|
||||||
features.forEach((feature) => {
|
features.forEach((feature) => {
|
||||||
|
if (feature.hooks) {
|
||||||
|
if (feature.hooks?.load?.length) {
|
||||||
|
sanitized.hooks.load = sanitized.hooks.load.concat(feature.hooks.load)
|
||||||
|
}
|
||||||
|
if (feature.hooks?.save?.length) {
|
||||||
|
sanitized.hooks.save = sanitized.hooks.save.concat(feature.hooks.save)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (feature.nodes?.length) {
|
if (feature.nodes?.length) {
|
||||||
sanitized.nodes = sanitized.nodes.concat(feature.nodes)
|
sanitized.nodes = sanitized.nodes.concat(feature.nodes)
|
||||||
feature.nodes.forEach((node) => {
|
feature.nodes.forEach((node) => {
|
||||||
|
|||||||
124
packages/richtext-lexical/src/field/lexical/utils/nodeFormat.ts
Normal file
124
packages/richtext-lexical/src/field/lexical/utils/nodeFormat.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/* eslint-disable perfectionist/sort-objects */
|
||||||
|
/* eslint-disable regexp/no-obscure-range */
|
||||||
|
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
|
||||||
|
//This copy-and-pasted from lexical here here: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts
|
||||||
|
|
||||||
|
import type { ElementFormatType, TextFormatType } from 'lexical'
|
||||||
|
import type { TextDetailType, TextModeType } from 'lexical/nodes/LexicalTextNode'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// DOM
|
||||||
|
export const NodeFormat = {
|
||||||
|
DOM_ELEMENT_TYPE: 1,
|
||||||
|
DOM_TEXT_TYPE: 3,
|
||||||
|
// Reconciling
|
||||||
|
NO_DIRTY_NODES: 0,
|
||||||
|
HAS_DIRTY_NODES: 1,
|
||||||
|
FULL_RECONCILE: 2,
|
||||||
|
// Text node modes
|
||||||
|
IS_NORMAL: 0,
|
||||||
|
IS_TOKEN: 1,
|
||||||
|
IS_SEGMENTED: 2,
|
||||||
|
IS_INERT: 3,
|
||||||
|
// Text node formatting
|
||||||
|
IS_BOLD: 1,
|
||||||
|
IS_ITALIC: 1 << 1,
|
||||||
|
IS_STRIKETHROUGH: 1 << 2,
|
||||||
|
IS_UNDERLINE: 1 << 3,
|
||||||
|
IS_CODE: 1 << 4,
|
||||||
|
IS_SUBSCRIPT: 1 << 5,
|
||||||
|
IS_SUPERSCRIPT: 1 << 6,
|
||||||
|
IS_HIGHLIGHT: 1 << 7,
|
||||||
|
// Text node details
|
||||||
|
IS_DIRECTIONLESS: 1,
|
||||||
|
IS_UNMERGEABLE: 1 << 1,
|
||||||
|
// Element node formatting
|
||||||
|
IS_ALIGN_LEFT: 1,
|
||||||
|
IS_ALIGN_CENTER: 2,
|
||||||
|
IS_ALIGN_RIGHT: 3,
|
||||||
|
IS_ALIGN_JUSTIFY: 4,
|
||||||
|
IS_ALIGN_START: 5,
|
||||||
|
IS_ALIGN_END: 6,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const IS_ALL_FORMATTING =
|
||||||
|
NodeFormat.IS_BOLD |
|
||||||
|
NodeFormat.IS_ITALIC |
|
||||||
|
NodeFormat.IS_STRIKETHROUGH |
|
||||||
|
NodeFormat.IS_UNDERLINE |
|
||||||
|
NodeFormat.IS_CODE |
|
||||||
|
NodeFormat.IS_SUBSCRIPT |
|
||||||
|
NodeFormat.IS_SUPERSCRIPT |
|
||||||
|
NodeFormat.IS_HIGHLIGHT
|
||||||
|
|
||||||
|
// Reconciliation
|
||||||
|
export const NON_BREAKING_SPACE = '\u00A0'
|
||||||
|
|
||||||
|
export const DOUBLE_LINE_BREAK = '\n\n'
|
||||||
|
|
||||||
|
// For FF, we need to use a non-breaking space, or it gets composition
|
||||||
|
// in a stuck state.
|
||||||
|
|
||||||
|
const RTL = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC'
|
||||||
|
const LTR =
|
||||||
|
'A-Za-z\u00C0-\u00D6\u00D8-\u00F6' +
|
||||||
|
'\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u200E\u2C00-\uFB1C' +
|
||||||
|
'\uFE00-\uFE6F\uFEFD-\uFFFF'
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-misleading-character-class
|
||||||
|
export const RTL_REGEX = new RegExp('^[^' + LTR + ']*[' + RTL + ']')
|
||||||
|
// eslint-disable-next-line no-misleading-character-class
|
||||||
|
export const LTR_REGEX = new RegExp('^[^' + RTL + ']*[' + LTR + ']')
|
||||||
|
|
||||||
|
export const TEXT_TYPE_TO_FORMAT: Record<TextFormatType | string, number> = {
|
||||||
|
bold: NodeFormat.IS_BOLD,
|
||||||
|
code: NodeFormat.IS_CODE,
|
||||||
|
highlight: NodeFormat.IS_HIGHLIGHT,
|
||||||
|
italic: NodeFormat.IS_ITALIC,
|
||||||
|
strikethrough: NodeFormat.IS_STRIKETHROUGH,
|
||||||
|
subscript: NodeFormat.IS_SUBSCRIPT,
|
||||||
|
superscript: NodeFormat.IS_SUPERSCRIPT,
|
||||||
|
underline: NodeFormat.IS_UNDERLINE,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DETAIL_TYPE_TO_DETAIL: Record<TextDetailType | string, number> = {
|
||||||
|
directionless: NodeFormat.IS_DIRECTIONLESS,
|
||||||
|
unmergeable: NodeFormat.IS_UNMERGEABLE,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ELEMENT_TYPE_TO_FORMAT: Record<Exclude<ElementFormatType, ''>, number> = {
|
||||||
|
center: NodeFormat.IS_ALIGN_CENTER,
|
||||||
|
end: NodeFormat.IS_ALIGN_END,
|
||||||
|
justify: NodeFormat.IS_ALIGN_JUSTIFY,
|
||||||
|
left: NodeFormat.IS_ALIGN_LEFT,
|
||||||
|
right: NodeFormat.IS_ALIGN_RIGHT,
|
||||||
|
start: NodeFormat.IS_ALIGN_START,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ELEMENT_FORMAT_TO_TYPE: Record<number, ElementFormatType> = {
|
||||||
|
[NodeFormat.IS_ALIGN_CENTER]: 'center',
|
||||||
|
[NodeFormat.IS_ALIGN_END]: 'end',
|
||||||
|
[NodeFormat.IS_ALIGN_JUSTIFY]: 'justify',
|
||||||
|
[NodeFormat.IS_ALIGN_LEFT]: 'left',
|
||||||
|
[NodeFormat.IS_ALIGN_RIGHT]: 'right',
|
||||||
|
[NodeFormat.IS_ALIGN_START]: 'start',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TEXT_MODE_TO_TYPE: Record<TextModeType, 0 | 1 | 2> = {
|
||||||
|
normal: NodeFormat.IS_NORMAL,
|
||||||
|
segmented: NodeFormat.IS_SEGMENTED,
|
||||||
|
token: NodeFormat.IS_TOKEN,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TEXT_TYPE_TO_MODE: Record<number, TextModeType> = {
|
||||||
|
[NodeFormat.IS_NORMAL]: 'normal',
|
||||||
|
[NodeFormat.IS_SEGMENTED]: 'segmented',
|
||||||
|
[NodeFormat.IS_TOKEN]: 'token',
|
||||||
|
}
|
||||||
@@ -153,6 +153,7 @@ export { IndentFeature } from './field/features/indent'
|
|||||||
export { CheckListFeature } from './field/features/lists/CheckList'
|
export { CheckListFeature } from './field/features/lists/CheckList'
|
||||||
export { OrderedListFeature } from './field/features/lists/OrderedList'
|
export { OrderedListFeature } from './field/features/lists/OrderedList'
|
||||||
export { UnoderedListFeature } from './field/features/lists/UnorderedList'
|
export { UnoderedListFeature } from './field/features/lists/UnorderedList'
|
||||||
|
export { SlateToLexicalFeature } from './field/features/migrations/SlateToLexical'
|
||||||
export type {
|
export type {
|
||||||
AfterReadPromise,
|
AfterReadPromise,
|
||||||
Feature,
|
Feature,
|
||||||
@@ -201,6 +202,20 @@ export { isHTMLElement } from './field/lexical/utils/guard'
|
|||||||
export { invariant } from './field/lexical/utils/invariant'
|
export { invariant } from './field/lexical/utils/invariant'
|
||||||
export { joinClasses } from './field/lexical/utils/joinClasses'
|
export { joinClasses } from './field/lexical/utils/joinClasses'
|
||||||
export { createBlockNode } from './field/lexical/utils/markdown/createBlockNode'
|
export { createBlockNode } from './field/lexical/utils/markdown/createBlockNode'
|
||||||
|
export {
|
||||||
|
DETAIL_TYPE_TO_DETAIL,
|
||||||
|
DOUBLE_LINE_BREAK,
|
||||||
|
ELEMENT_FORMAT_TO_TYPE,
|
||||||
|
ELEMENT_TYPE_TO_FORMAT,
|
||||||
|
IS_ALL_FORMATTING,
|
||||||
|
LTR_REGEX,
|
||||||
|
NON_BREAKING_SPACE,
|
||||||
|
NodeFormat,
|
||||||
|
RTL_REGEX,
|
||||||
|
TEXT_MODE_TO_TYPE,
|
||||||
|
TEXT_TYPE_TO_FORMAT,
|
||||||
|
TEXT_TYPE_TO_MODE,
|
||||||
|
} from './field/lexical/utils/nodeFormat'
|
||||||
export { Point, isPoint } from './field/lexical/utils/point'
|
export { Point, isPoint } from './field/lexical/utils/point'
|
||||||
export { Rect } from './field/lexical/utils/rect'
|
export { Rect } from './field/lexical/utils/rect'
|
||||||
export { setFloatingElemPosition } from './field/lexical/utils/setFloatingElemPosition'
|
export { setFloatingElemPosition } from './field/lexical/utils/setFloatingElemPosition'
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const LexicalFields: CollectionConfig = {
|
|||||||
slug: 'lexical-fields',
|
slug: 'lexical-fields',
|
||||||
admin: {
|
admin: {
|
||||||
useAsTitle: 'title',
|
useAsTitle: 'title',
|
||||||
|
listSearchableFields: ['title', 'richTextLexicalCustomFields'],
|
||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
read: () => true,
|
read: () => true,
|
||||||
|
|||||||
Reference in New Issue
Block a user