Compare commits
17 Commits
richtext-l
...
richtext-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
647fe23d1c | ||
|
|
d7c61861f6 | ||
|
|
7caa098023 | ||
|
|
fd54c40400 | ||
|
|
e180131314 | ||
|
|
5902d4542b | ||
|
|
6bc282444e | ||
|
|
4dc6c09347 | ||
|
|
03b9ab0054 | ||
|
|
3c3c93f483 | ||
|
|
5dbfb1a335 | ||
|
|
d411874589 | ||
|
|
8358e2f2d2 | ||
|
|
012b8e6f90 | ||
|
|
fcd4c8d830 | ||
|
|
81ec435363 | ||
|
|
e116fcfbf5 |
@@ -129,7 +129,7 @@ To add a _new_ view to the Admin Panel, simply add another key to the `views` ob
|
||||
}
|
||||
```
|
||||
|
||||
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/master/test/admin/components)._
|
||||
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/main/test/admin/components)._
|
||||
|
||||
For help on how to build your own custom view components, see [building a custom view component](#building-a-custom-view-component).
|
||||
|
||||
@@ -399,12 +399,12 @@ Your custom view components will be given all the props that a React Router `<Ro
|
||||
|
||||
#### Example
|
||||
|
||||
You can find examples of custom views in the [Payload source code `/test/admin/components/views` folder](https://github.com/payloadcms/payload/tree/master/test/admin/components/views). There, you'll find two custom views:
|
||||
You can find examples of custom views in the [Payload source code `/test/admin/components/views` folder](https://github.com/payloadcms/payload/tree/main/test/admin/components/views). There, you'll find two custom views:
|
||||
|
||||
1. A custom view that uses the `DefaultTemplate`, which is the built-in Payload template that displays the sidebar and "eyebrow nav"
|
||||
1. A custom view that uses the `MinimalTemplate` - which is just a centered template used for things like logging in or out
|
||||
|
||||
To see how to pass in your custom views to create custom views of your own, take a look at the `admin.components.views` property of the [Payload test admin config](https://github.com/payloadcms/payload/blob/master/test/admin/config.ts).
|
||||
To see how to pass in your custom views to create custom views of your own, take a look at the `admin.components.views` property of the [Payload test admin config](https://github.com/payloadcms/payload/blob/main/test/admin/config.ts).
|
||||
|
||||
### Fields
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
"lint-staged": "lint-staged",
|
||||
"pretest": "pnpm build",
|
||||
"reinstall": "pnpm clean:unix && pnpm install",
|
||||
"list:packages": "./scripts/list_published_packages.sh beta",
|
||||
"script:release:beta": "./scripts/release_beta.sh",
|
||||
"script:list-packages": "tsx ./scripts/list-packages.ts",
|
||||
"script:release": "tsx ./scripts/release.ts",
|
||||
"test": "pnpm test:int && pnpm test:components && pnpm test:e2e",
|
||||
"test:components": "cross-env jest --config=jest.components.config.js",
|
||||
"test:e2e": "npx playwright install --with-deps && ts-node -T ./test/runE2E.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Connect } from 'payload/database'
|
||||
|
||||
import { pushSchema } from 'drizzle-kit/utils'
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { numeric, pgTable, timestamp, varchar } from 'drizzle-orm/pg-core'
|
||||
@@ -40,6 +39,8 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
|
||||
)
|
||||
return
|
||||
|
||||
const { pushSchema } = require('drizzle-kit/utils')
|
||||
|
||||
// This will prompt if clarifications are needed for Drizzle to push new schema
|
||||
const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema(
|
||||
this.schema,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import type { DrizzleSnapshotJSON } from 'drizzle-kit/utils'
|
||||
import type { CreateMigration } from 'payload/database'
|
||||
|
||||
import { generateDrizzleJson, generateMigration } from 'drizzle-kit/utils'
|
||||
import fs from 'fs'
|
||||
import prompts from 'prompts'
|
||||
|
||||
@@ -61,6 +60,8 @@ export const createMigration: CreateMigration = async function createMigration(
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
|
||||
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/utils')
|
||||
|
||||
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
|
||||
const formattedDate = yyymmdd.replace(/\D/g, '')
|
||||
const formattedTime = hhmmss.split('.')[0].replace(/\D/g, '')
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
import type { Payload } from 'payload'
|
||||
import type { Migration } from 'payload/database'
|
||||
|
||||
import { generateDrizzleJson } from 'drizzle-kit/utils'
|
||||
import { readMigrationFiles } from 'payload/database'
|
||||
import { DatabaseError } from 'pg'
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { PostgresAdapter } from './types'
|
||||
@@ -78,6 +76,8 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
|
||||
}
|
||||
|
||||
async function runMigrationFile(payload: Payload, migration: Migration, batch: number) {
|
||||
const { generateDrizzleJson } = require('drizzle-kit/utils')
|
||||
|
||||
const start = Date.now()
|
||||
|
||||
payload.logger.info({ msg: `Migrating: ${migration.name}` })
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
|
||||
// drizzle-kit@utils
|
||||
|
||||
import { generateDrizzleJson, generateMigration, pushSchema } from 'drizzle-kit/utils'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { Pool } from 'pg'
|
||||
|
||||
async function generateUsage() {
|
||||
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/utils')
|
||||
|
||||
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
|
||||
const schema = await import('./data/users')
|
||||
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
|
||||
@@ -25,6 +26,8 @@ async function generateUsage() {
|
||||
}
|
||||
|
||||
async function pushUsage() {
|
||||
const { pushSchema } = require('drizzle-kit/utils')
|
||||
|
||||
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
|
||||
const schemaAfter = await import('./data/users-after')
|
||||
|
||||
|
||||
@@ -208,8 +208,7 @@
|
||||
"webpack": "^5.78.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"pnpm": ">=8"
|
||||
"node": ">=14"
|
||||
},
|
||||
"files": [
|
||||
"bin.js",
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
position: sticky;
|
||||
top: var(--doc-controls-height);
|
||||
width: 33.33%;
|
||||
height: 100%;
|
||||
height: calc(100vh - var(--doc-controls-height));
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
position: sticky;
|
||||
top: var(--doc-controls-height);
|
||||
width: 33.33%;
|
||||
height: 100%;
|
||||
height: calc(100vh - var(--doc-controls-height));
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
|
||||
@@ -382,7 +382,7 @@ export const relationship: Validate<unknown, unknown, RelationshipField> = async
|
||||
})
|
||||
|
||||
if (invalidRelationships.length > 0) {
|
||||
return `This field has the following invalid selections: ${invalidRelationships
|
||||
return `This relationship field has the following invalid relationships: ${invalidRelationships
|
||||
.map((err, invalid) => {
|
||||
return `${err} ${JSON.stringify(invalid)}`
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.9",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Block } from 'payload/types'
|
||||
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
|
||||
import type { BlocksFeatureProps } from '.'
|
||||
@@ -20,40 +22,42 @@ export const blockAfterReadPromiseHOC = (
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
}) => {
|
||||
const blocks: Block[] = props.blocks
|
||||
const blockFieldData = node.fields.data
|
||||
|
||||
const promises: Promise<void>[] = []
|
||||
|
||||
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
|
||||
const payloadConfig = req.payload.config
|
||||
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
|
||||
props.blocks = props.blocks.map((block) => {
|
||||
const unsanitizedBlock = { ...block }
|
||||
unsanitizedBlock.fields = sanitizeFields({
|
||||
blocks.forEach((block) => {
|
||||
block.fields = sanitizeFields({
|
||||
config: payloadConfig,
|
||||
fields: block.fields,
|
||||
validRelationships,
|
||||
})
|
||||
return unsanitizedBlock
|
||||
})
|
||||
|
||||
if (Array.isArray(props.blocks)) {
|
||||
props.blocks.forEach((block) => {
|
||||
if (block?.fields) {
|
||||
recurseNestedFields({
|
||||
afterReadPromises,
|
||||
currentDepth,
|
||||
data: node.fields.data || {},
|
||||
depth,
|
||||
fields: block.fields,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
})
|
||||
}
|
||||
})
|
||||
// find block used in this node
|
||||
const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
|
||||
if (!block || !block?.fields?.length || !blockFieldData) {
|
||||
return promises
|
||||
}
|
||||
|
||||
recurseNestedFields({
|
||||
afterReadPromises,
|
||||
currentDepth,
|
||||
data: blockFieldData,
|
||||
depth,
|
||||
fields: block.fields,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
// The afterReadPromise gets its data from looking for field.name inside of the siblingDoc. Thus, here we cannot pass the whole document's siblingDoc, but only the siblingDoc (sibling fields) of the current field.
|
||||
siblingDoc: blockFieldData,
|
||||
})
|
||||
|
||||
return promises
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@ export const blockValidationHOC = (
|
||||
payloadConfig,
|
||||
validation,
|
||||
}) => {
|
||||
const blockFieldValues = node.fields.data
|
||||
|
||||
const blockFieldData = node.fields.data
|
||||
const blocks: Block[] = props.blocks
|
||||
|
||||
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
|
||||
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
|
||||
blocks.forEach((block) => {
|
||||
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
|
||||
block.fields = sanitizeFields({
|
||||
config: payloadConfig,
|
||||
fields: block.fields,
|
||||
@@ -29,7 +29,7 @@ export const blockValidationHOC = (
|
||||
})
|
||||
|
||||
// find block
|
||||
const block = props.blocks.find((block) => block.slug === blockFieldValues.blockType)
|
||||
const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
|
||||
|
||||
// validate block
|
||||
if (!block) {
|
||||
|
||||
@@ -52,7 +52,7 @@ export const linkAfterReadPromiseHOC = (
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
siblingDoc: node.fields || {},
|
||||
})
|
||||
}
|
||||
return promises
|
||||
|
||||
@@ -51,7 +51,7 @@ export const uploadAfterReadPromiseHOC = (
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
siblingDoc: node.fields || {},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -173,7 +173,7 @@ export const recurseNestedFields = ({
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
siblingDoc: data[field.name][i], // This has to be scoped to the blocks's fields, otherwise there may be population issues, e.g. for a relationship field with Blocks Node, with a Blocks Field, with a RichText Field, With Relationship Node. The last richtext field would try to find itself using siblingDoc[field.nane], which only works if the siblingDoc is scoped to the blocks's fields
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -191,14 +191,13 @@ export const recurseNestedFields = ({
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
siblingDoc, // TODO: if there's any population issues, this might have to be data[field.name][i] as well
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'richText') {
|
||||
// TODO: This does not properly work yet. E.g. it does not handle a relationship inside of lexical inside of block inside of lexical
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.afterReadPromise) {
|
||||
|
||||
@@ -59,7 +59,7 @@ export const recurseRichText = ({
|
||||
}
|
||||
}
|
||||
|
||||
if ('children' in node && node?.children) {
|
||||
if ('children' in node && Array.isArray(node?.children) && node?.children?.length) {
|
||||
recurseRichText({
|
||||
afterReadPromises,
|
||||
children: node.children as SerializedLexicalNode[],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-slate",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"description": "The officially supported Slate richtext adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import type { CellComponentProps, RichTextField } from 'payload/types'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import type { ElementType } from 'react'
|
||||
|
||||
import { Tooltip } from 'payload/components'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import { Editor, Transforms } from 'slate'
|
||||
import { ReactEditor } from 'slate-react'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import { ShimmerEffect } from 'payload/components'
|
||||
import React, { Suspense, lazy } from 'react'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useSlate } from 'slate-react'
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# List all published packages
|
||||
|
||||
packages=$(find packages -name package.json -type f -exec grep -L '"private": true' {} \; | xargs jq -r '.name')
|
||||
|
||||
# sort alphabetically
|
||||
packages=$(echo "$packages" | tr ' ' '\n' | sort -u | tr '\n' ' ')
|
||||
|
||||
# Loop through each package and print the name and version. Print as table
|
||||
|
||||
printf "%-30s %-20s %-20s\n" "package" "latest" "beta"
|
||||
|
||||
for package in $packages; do
|
||||
info=$(npm view "$package" dist-tags --json)
|
||||
latest=$(echo "$info" | jq -r '.latest')
|
||||
beta=$(echo "$info" | jq -r '.beta')
|
||||
printf "%-30s %-20s %-20s\n" "$package" "$latest" "$beta"
|
||||
done
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
# Build packages/payload
|
||||
|
||||
package_name=$1
|
||||
package_dir="packages/$package_name"
|
||||
|
||||
if [ -z "$package_name" ]; then
|
||||
echo "Please specify a package to publish"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if packages/$package_name exists
|
||||
|
||||
if [ ! -d "$package_dir" ]; then
|
||||
echo "Package $package_name does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npm --prefix "$package_dir" version pre --preid beta
|
||||
git add "$package_dir"/package.json
|
||||
new_version=$(node -p "require('./$package_dir/package.json').version")
|
||||
git commit -m "chore(release): $package_name@$new_version"
|
||||
pnpm publish -C "$package_dir" --tag beta --no-git-checks
|
||||
@@ -65,6 +65,24 @@ export const Posts: CollectionConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'relationship',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'sidebarField',
|
||||
type: 'text',
|
||||
|
||||
@@ -330,10 +330,12 @@ describe('admin', () => {
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
const options = page.locator('.rs__option')
|
||||
const titleOption = options.locator('text=Title')
|
||||
|
||||
await expect(titleOption).toHaveText('Title')
|
||||
const titleOption = page.locator('.rs__option', {
|
||||
hasText: exactText('Title'),
|
||||
})
|
||||
|
||||
await expect(titleOption).toBeVisible()
|
||||
|
||||
await titleOption.click()
|
||||
const titleInput = page.locator('#field-title')
|
||||
|
||||
@@ -13,6 +13,17 @@ export const TextBlock: Block = {
|
||||
slug: 'text',
|
||||
}
|
||||
|
||||
export const RichTextBlock: Block = {
|
||||
fields: [
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
slug: 'richText',
|
||||
}
|
||||
|
||||
export const UploadAndRichTextBlock: Block = {
|
||||
fields: [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { loremIpsum } from './loremIpsum'
|
||||
|
||||
export function generateLexicalRichText() {
|
||||
return {
|
||||
root: {
|
||||
@@ -15,210 +13,7 @@ export function generateLexicalRichText() {
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: "Hello, I'm a rich text field.",
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: 'center',
|
||||
indent: 0,
|
||||
type: 'heading',
|
||||
version: 1,
|
||||
tag: 'h1',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'I can do all kinds of fun stuff like ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'render links',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'link',
|
||||
version: 1,
|
||||
fields: {
|
||||
url: 'https://payloadcms.com',
|
||||
newTab: true,
|
||||
linkType: 'custom',
|
||||
},
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ', ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'link to relationships',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'link',
|
||||
version: 1,
|
||||
fields: {
|
||||
url: 'https://',
|
||||
doc: {
|
||||
value: {
|
||||
id: '{{ARRAY_DOC_ID}}',
|
||||
},
|
||||
relationTo: 'array-fields',
|
||||
},
|
||||
newTab: false,
|
||||
linkType: 'internal',
|
||||
},
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ', and store nested relationship fields:',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
type: 'relationship',
|
||||
version: 1,
|
||||
value: {
|
||||
id: '{{TEXT_DOC_ID}}',
|
||||
},
|
||||
relationTo: 'text-fields',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'You can build your own elements, too.',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: "It's built with Lexical",
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'It stores content as JSON so you can use it wherever you need',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: "It's got a great editing experience for non-technical users",
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
value: 3,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'list',
|
||||
version: 1,
|
||||
listType: 'bullet',
|
||||
start: 1,
|
||||
tag: 'ul',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'And a whole lot more.',
|
||||
text: 'Upload Node:',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
@@ -233,10 +28,6 @@ export function generateLexicalRichText() {
|
||||
format: '',
|
||||
type: 'upload',
|
||||
version: 1,
|
||||
relationTo: 'uploads',
|
||||
value: {
|
||||
id: '{{UPLOAD_DOC_ID}}',
|
||||
},
|
||||
fields: {
|
||||
caption: {
|
||||
root: {
|
||||
@@ -245,69 +36,151 @@ export function generateLexicalRichText() {
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: [
|
||||
...[...Array(4)].map(() => ({
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: loremIpsum,
|
||||
text: 'Relationship inside Upload Caption:',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
})),
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
type: 'relationship',
|
||||
version: 1,
|
||||
relationTo: 'text-fields',
|
||||
value: {
|
||||
id: '{{TEXT_DOC_ID}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
},
|
||||
},
|
||||
},
|
||||
relationTo: 'uploads',
|
||||
value: {
|
||||
id: '{{UPLOAD_DOC_ID}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
type: 'block',
|
||||
version: 1,
|
||||
fields: {
|
||||
data: {
|
||||
id: '65298b13db4ef8c744a7faaa',
|
||||
rel: '{{UPLOAD_DOC_ID}}',
|
||||
blockName: 'Block Node, with Relationship Field',
|
||||
blockType: 'relationshipBlock',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
type: 'block',
|
||||
version: 1,
|
||||
fields: {
|
||||
data: {
|
||||
id: '65298b1ddb4ef8c744a7faab',
|
||||
richText: {
|
||||
root: {
|
||||
type: 'root',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: [
|
||||
{
|
||||
format: '',
|
||||
type: 'relationship',
|
||||
version: 1,
|
||||
relationTo: 'text-fields',
|
||||
value: {
|
||||
id: '{{TEXT_DOC_ID}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
},
|
||||
},
|
||||
blockName: 'Block Node, with RichText Field, with Relationship Node',
|
||||
blockType: 'richText',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
type: 'block',
|
||||
version: 1,
|
||||
fields: {
|
||||
data: {
|
||||
id: '65298b2bdb4ef8c744a7faac',
|
||||
blockName:
|
||||
'Block Node, with Blocks Field, With RichText Field, With Relationship Node',
|
||||
blockType: 'subBlock',
|
||||
subBlocks: [
|
||||
{
|
||||
id: '65298b2edb4ef8c744a7faad',
|
||||
richText: {
|
||||
root: {
|
||||
type: 'root',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: [
|
||||
{
|
||||
format: '',
|
||||
type: 'relationship',
|
||||
version: 1,
|
||||
relationTo: 'text-fields',
|
||||
value: {
|
||||
id: '{{TEXT_DOC_ID}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
},
|
||||
},
|
||||
blockType: 'contentBlock',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
type: 'block',
|
||||
version: 1,
|
||||
fields: {
|
||||
data: {
|
||||
id: '65298b49db4ef8c744a7faae',
|
||||
upload: '{{UPLOAD_DOC_ID}}',
|
||||
blockName: 'Block Node, With Upload Field',
|
||||
blockType: 'uploadAndRichText',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
direction: 'ltr',
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam hendrerit nisi sed sollicitudin pellentesque. Nunc posuere purus rhoncus pulvinar aliquam. Ut aliquet tristique nisl vitae volutpat. Nulla aliquet porttitor venenatis. Donec a dui et dui fringilla consectetur id nec massa. Aliquam erat volutpat. Sed ut dui ut lacus dictum fermentum vel tincidunt neque. Sed sed lacinia lectus. Duis sit amet sodales felis. Duis nunc eros, mattis at dui ac, convallis semper risus. In adipiscing ultrices tellus, in suscipit massa vehicula eu.',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam hendrerit nisi sed sollicitudin pellentesque. Nunc posuere purus rhoncus pulvinar aliquam. Ut aliquet tristique nisl vitae volutpat. Nulla aliquet porttitor venenatis. Donec a dui et dui fringilla consectetur id nec massa. Aliquam erat volutpat. Sed ut dui ut lacus dictum fermentum vel tincidunt neque. Sed sed lacinia lectus. Duis sit amet sodales felis. Duis nunc eros, mattis at dui ac, convallis semper risus. In adipiscing ultrices tellus, in suscipit massa vehicula eu.',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
children: [],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '../../../../packages/richtext-lexical/src'
|
||||
import {
|
||||
RelationshipBlock,
|
||||
RichTextBlock,
|
||||
SelectFieldBlock,
|
||||
SubBlockBlock,
|
||||
TextBlock,
|
||||
@@ -20,6 +21,7 @@ export const LexicalFields: CollectionConfig = {
|
||||
slug: 'lexical-fields',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
listSearchableFields: ['title', 'richTextLexicalCustomFields'],
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
@@ -68,6 +70,7 @@ export const LexicalFields: CollectionConfig = {
|
||||
}),
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
RichTextBlock,
|
||||
TextBlock,
|
||||
UploadAndRichTextBlock,
|
||||
SelectFieldBlock,
|
||||
@@ -81,7 +84,7 @@ export const LexicalFields: CollectionConfig = {
|
||||
],
|
||||
}
|
||||
|
||||
export const lexicalRichTextDoc = {
|
||||
export const LexicalRichTextDoc = {
|
||||
title: 'Rich Text',
|
||||
richTextLexicalCustomFields: generateLexicalRichText(),
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import DateFields, { dateDoc } from './collections/Date'
|
||||
import GroupFields, { groupDoc } from './collections/Group'
|
||||
import IndexedFields from './collections/Indexed'
|
||||
import JSONFields, { jsonDoc } from './collections/JSON'
|
||||
import { LexicalFields } from './collections/Lexical'
|
||||
import { LexicalFields, LexicalRichTextDoc } from './collections/Lexical'
|
||||
import NumberFields, { numberDoc } from './collections/Number'
|
||||
import PointFields, { pointDoc } from './collections/Point'
|
||||
import RadioFields, { radiosDoc } from './collections/Radio'
|
||||
@@ -150,7 +150,7 @@ export default buildConfigWithDefaults({
|
||||
)
|
||||
|
||||
const lexicalRichTextDocWithRelId = JSON.parse(
|
||||
JSON.stringify(richTextDoc)
|
||||
JSON.stringify(LexicalRichTextDoc)
|
||||
.replace(/"\{\{ARRAY_DOC_ID\}\}"/g, formattedID)
|
||||
.replace(/"\{\{UPLOAD_DOC_ID\}\}"/g, formattedJPGID)
|
||||
.replace(/"\{\{TEXT_DOC_ID\}\}"/g, formattedTextID),
|
||||
|
||||
Reference in New Issue
Block a user