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...')
|
||||
|
||||
useEffect(() => {
|
||||
if (data == null) {
|
||||
let dataToUse = data
|
||||
if (dataToUse == null) {
|
||||
setPreview('')
|
||||
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
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
namespace: editorConfig.lexical.namespace,
|
||||
nodes: getEnabledNodes({ editorConfig }),
|
||||
theme: editorConfig.lexical.theme,
|
||||
})
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(data))
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse))
|
||||
|
||||
const textContent =
|
||||
headlessEditor.getEditorState().read(() => {
|
||||
|
||||
@@ -89,9 +89,16 @@ const RichText: React.FC<FieldProps> = (props) => {
|
||||
fieldProps={props}
|
||||
initialState={initialValue}
|
||||
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}
|
||||
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?: {
|
||||
sections: FloatingToolbarSection[]
|
||||
}
|
||||
hooks?: {
|
||||
load?: ({
|
||||
incomingEditorState,
|
||||
}: {
|
||||
incomingEditorState: SerializedEditorState
|
||||
}) => SerializedEditorState
|
||||
save?: ({
|
||||
incomingEditorState,
|
||||
}: {
|
||||
incomingEditorState: SerializedEditorState
|
||||
}) => SerializedEditorState
|
||||
}
|
||||
markdownTransformers?: Transformer[]
|
||||
nodes?: Array<{
|
||||
afterReadPromises?: Array<AfterReadPromise>
|
||||
@@ -123,6 +135,22 @@ export type SanitizedFeatures = Required<
|
||||
floatingSelectToolbar: {
|
||||
sections: FloatingToolbarSection[]
|
||||
}
|
||||
hooks: {
|
||||
load: Array<
|
||||
({
|
||||
incomingEditorState,
|
||||
}: {
|
||||
incomingEditorState: SerializedEditorState
|
||||
}) => SerializedEditorState
|
||||
>
|
||||
save: Array<
|
||||
({
|
||||
incomingEditorState,
|
||||
}: {
|
||||
incomingEditorState: SerializedEditorState
|
||||
}) => SerializedEditorState
|
||||
>
|
||||
}
|
||||
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
|
||||
|
||||
@@ -21,7 +21,16 @@ export type LexicalProviderProps = {
|
||||
value: SerializedEditorState
|
||||
}
|
||||
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 (
|
||||
(value && Array.isArray(value) && !('root' in value)) ||
|
||||
|
||||
@@ -10,6 +10,10 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
||||
floatingSelectToolbar: {
|
||||
sections: [],
|
||||
},
|
||||
hooks: {
|
||||
load: [],
|
||||
save: [],
|
||||
},
|
||||
markdownTransformers: [],
|
||||
nodes: [],
|
||||
plugins: [],
|
||||
@@ -21,6 +25,15 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
||||
}
|
||||
|
||||
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) {
|
||||
sanitized.nodes = sanitized.nodes.concat(feature.nodes)
|
||||
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 { OrderedListFeature } from './field/features/lists/OrderedList'
|
||||
export { UnoderedListFeature } from './field/features/lists/UnorderedList'
|
||||
export { SlateToLexicalFeature } from './field/features/migrations/SlateToLexical'
|
||||
export type {
|
||||
AfterReadPromise,
|
||||
Feature,
|
||||
@@ -201,6 +202,20 @@ export { isHTMLElement } from './field/lexical/utils/guard'
|
||||
export { invariant } from './field/lexical/utils/invariant'
|
||||
export { joinClasses } from './field/lexical/utils/joinClasses'
|
||||
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 { Rect } from './field/lexical/utils/rect'
|
||||
export { setFloatingElemPosition } from './field/lexical/utils/setFloatingElemPosition'
|
||||
|
||||
@@ -21,6 +21,7 @@ export const LexicalFields: CollectionConfig = {
|
||||
slug: 'lexical-fields',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
listSearchableFields: ['title', 'richTextLexicalCustomFields'],
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
|
||||
Reference in New Issue
Block a user