feat(richtext-lexical): SlateToLexical migration feature

This commit is contained in:
Alessio Gravili
2023-10-14 13:36:32 +02:00
parent 03b9ab0054
commit 4dc6c09347
23 changed files with 837 additions and 5 deletions

View File

@@ -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(() => {

View File

@@ -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}

View File

@@ -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'],
}

View File

@@ -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'],
}

View File

@@ -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'],
}

View File

@@ -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'],
}

View File

@@ -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'],
}

View File

@@ -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'],
}

View File

@@ -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'],
}

View File

@@ -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'],
}

View File

@@ -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'],
}

View File

@@ -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,
]

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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',
}
}

View File

@@ -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;
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)) ||

View File

@@ -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) => {

View 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',
}

View File

@@ -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'

View File

@@ -21,6 +21,7 @@ export const LexicalFields: CollectionConfig = {
slug: 'lexical-fields',
admin: {
useAsTitle: 'title',
listSearchableFields: ['title', 'richTextLexicalCustomFields'],
},
access: {
read: () => true,