chore: server-side rendered rich text elements
This commit is contained in:
@@ -33,7 +33,14 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
|
|||||||
|
|
||||||
const fieldSchemaMap = getFieldSchemaMap(req.payload.config)
|
const fieldSchemaMap = getFieldSchemaMap(req.payload.config)
|
||||||
|
|
||||||
const { id, operation, docPreferences, formState, schemaPath } = reqData as BuildFormStateArgs
|
const {
|
||||||
|
id,
|
||||||
|
operation,
|
||||||
|
docPreferences,
|
||||||
|
formState,
|
||||||
|
data: incomingData,
|
||||||
|
schemaPath,
|
||||||
|
} = reqData as BuildFormStateArgs
|
||||||
const schemaPathSegments = schemaPath.split('.')
|
const schemaPathSegments = schemaPath.split('.')
|
||||||
|
|
||||||
let fieldSchema: Field[]
|
let fieldSchema: Field[]
|
||||||
@@ -59,7 +66,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = reduceFieldsToValues(formState, true)
|
const data = incomingData || reduceFieldsToValues(formState, true)
|
||||||
|
|
||||||
const result = await buildStateFromSchema({
|
const result = await buildStateFromSchema({
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { JSONSchema4 } from 'json-schema'
|
import type { JSONSchema4 } from 'json-schema'
|
||||||
|
|
||||||
import type { RichTextField, Validate } from '../fields/config/types'
|
import type { SanitizedConfig } from '../config/types'
|
||||||
|
import type { Field, RichTextField, Validate } from '../fields/config/types'
|
||||||
import type { PayloadRequest, RequestContext } from '../types'
|
import type { PayloadRequest, RequestContext } from '../types'
|
||||||
import type { CellComponentProps } from './elements/Cell'
|
import type { CellComponentProps } from './elements/Cell'
|
||||||
|
|
||||||
@@ -26,7 +27,15 @@ type RichTextAdapterBase<
|
|||||||
incomingEditorState: Value
|
incomingEditorState: Value
|
||||||
siblingDoc: Record<string, unknown>
|
siblingDoc: Record<string, unknown>
|
||||||
}) => Promise<void> | null
|
}) => Promise<void> | null
|
||||||
generateComponentMap: () => Map<string, React.ReactNode> | Promise<Map<string, React.ReactNode>>
|
generateComponentMap: (args: {
|
||||||
|
config: SanitizedConfig
|
||||||
|
schemaPath: string
|
||||||
|
}) => Map<string, React.ReactNode>
|
||||||
|
generateSchemaMap: (args: {
|
||||||
|
config: SanitizedConfig
|
||||||
|
schemaMap: Map<string, Field[]>
|
||||||
|
schemaPath: string
|
||||||
|
}) => Map<string, Field[]>
|
||||||
outputSchema?: ({
|
outputSchema?: ({
|
||||||
field,
|
field,
|
||||||
isRequired,
|
isRequired,
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ import type {
|
|||||||
} from '../../collections/config/types'
|
} from '../../collections/config/types'
|
||||||
import type { PaginatedDocs, TypeWithVersion } from '../../exports/database'
|
import type { PaginatedDocs, TypeWithVersion } from '../../exports/database'
|
||||||
import type { SanitizedGlobalConfig } from '../../globals/config/types'
|
import type { SanitizedGlobalConfig } from '../../globals/config/types'
|
||||||
|
import type { DocumentPreferences } from '../../preferences/types'
|
||||||
|
|
||||||
export type DocumentInfoContext = {
|
export type DocumentInfoContext = {
|
||||||
collectionSlug?: SanitizedCollectionConfig['slug']
|
collectionSlug?: SanitizedCollectionConfig['slug']
|
||||||
docConfig?: SanitizedCollectionConfig | SanitizedGlobalConfig
|
docConfig?: SanitizedCollectionConfig | SanitizedGlobalConfig
|
||||||
docPermissions: DocumentPermissions
|
docPermissions: DocumentPermissions
|
||||||
getDocPermissions: () => Promise<void>
|
getDocPermissions: () => Promise<void>
|
||||||
getDocPreferences: () => Promise<{ [key: string]: unknown }>
|
getDocPreferences: () => Promise<DocumentPreferences>
|
||||||
getVersions: () => Promise<void>
|
getVersions: () => Promise<void>
|
||||||
globalSlug?: SanitizedGlobalConfig['slug']
|
globalSlug?: SanitizedGlobalConfig['slug']
|
||||||
id?: number | string
|
id?: number | string
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { SendMailOptions } from 'nodemailer'
|
|||||||
import type pino from 'pino'
|
import type pino from 'pino'
|
||||||
|
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
import type { AuthStrategy } from './auth'
|
import type { AuthStrategy } from './auth'
|
||||||
import type { Result as ForgotPasswordResult } from './auth/operations/forgotPassword'
|
import type { Result as ForgotPasswordResult } from './auth/operations/forgotPassword'
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const extractTranslations = (keys: string[]): Record<string, Record<strin
|
|||||||
if (resource?.[section]?.[target]) {
|
if (resource?.[section]?.[target]) {
|
||||||
result[key][language] = resource[section][target]
|
result[key][language] = resource[section][target]
|
||||||
} else {
|
} else {
|
||||||
console.error(`Missing translation for ${key} in ${language}`)
|
// console.error(`Missing translation for ${key} in ${language}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,8 +18,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@faceless-ui/modal": "2.0.1",
|
"@faceless-ui/modal": "2.0.1",
|
||||||
"@payloadcms/translations": "workspace:^",
|
|
||||||
"@payloadcms/ui": "workspace:^",
|
|
||||||
"is-hotkey": "0.2.0",
|
"is-hotkey": "0.2.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"slate": "0.91.4",
|
"slate": "0.91.4",
|
||||||
@@ -34,7 +32,9 @@
|
|||||||
"payload": "workspace:*"
|
"payload": "workspace:*"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"payload": "^2.3.0"
|
"payload": "^2.3.0",
|
||||||
|
"@payloadcms/translations": "workspace:^",
|
||||||
|
"@payloadcms/ui": "workspace:^"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import { withHistory } from 'slate-history'
|
|||||||
import { Editable, Slate, withReact } from 'slate-react'
|
import { Editable, Slate, withReact } from 'slate-react'
|
||||||
|
|
||||||
import type { FormFieldBase } from '../../../ui/src/forms/fields/shared'
|
import type { FormFieldBase } from '../../../ui/src/forms/fields/shared'
|
||||||
import type { ElementNode, RichTextElement, RichTextLeaf, TextNode } from '../types'
|
import type { ElementNode, TextNode } from '../types'
|
||||||
|
import type { EnabledFeatures } from './types'
|
||||||
|
|
||||||
import { withCondition } from '../../../ui/src/forms/withCondition'
|
import { withCondition } from '../../../ui/src/forms/withCondition'
|
||||||
import { defaultRichTextValue } from '../data/defaultValue'
|
import { defaultRichTextValue } from '../data/defaultValue'
|
||||||
@@ -24,25 +25,11 @@ import './index.scss'
|
|||||||
import toggleLeaf from './leaves/toggle'
|
import toggleLeaf from './leaves/toggle'
|
||||||
import withEnterBreakOut from './plugins/withEnterBreakOut'
|
import withEnterBreakOut from './plugins/withEnterBreakOut'
|
||||||
import withHTML from './plugins/withHTML'
|
import withHTML from './plugins/withHTML'
|
||||||
|
import { ElementButtonProvider } from './providers/ElementButtonProvider'
|
||||||
|
import { ElementProvider } from './providers/ElementProvider'
|
||||||
import { LeafButtonProvider } from './providers/LeafButtonProvider'
|
import { LeafButtonProvider } from './providers/LeafButtonProvider'
|
||||||
import { LeafProvider } from './providers/LeafProvider'
|
import { LeafProvider } from './providers/LeafProvider'
|
||||||
|
|
||||||
const defaultElements: RichTextElement[] = [
|
|
||||||
'h1',
|
|
||||||
'h2',
|
|
||||||
'h3',
|
|
||||||
'h4',
|
|
||||||
'h5',
|
|
||||||
'h6',
|
|
||||||
'ul',
|
|
||||||
'ol',
|
|
||||||
'indent',
|
|
||||||
'link',
|
|
||||||
'relationship',
|
|
||||||
'upload',
|
|
||||||
]
|
|
||||||
const defaultLeaves: RichTextLeaf[] = ['bold', 'italic', 'underline', 'strikethrough', 'code']
|
|
||||||
|
|
||||||
const baseClass = 'rich-text'
|
const baseClass = 'rich-text'
|
||||||
|
|
||||||
declare module 'slate' {
|
declare module 'slate' {
|
||||||
@@ -75,38 +62,47 @@ const RichText: React.FC<
|
|||||||
width,
|
width,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const [leaves] = useState(() => {
|
const [{ elements, leaves }] = useState<EnabledFeatures>(() => {
|
||||||
const enabledLeaves: Record<
|
const features: EnabledFeatures = {
|
||||||
string,
|
elements: {},
|
||||||
{
|
leaves: {},
|
||||||
Button: React.ReactNode
|
}
|
||||||
Leaf: React.ReactNode
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
> = {}
|
|
||||||
|
|
||||||
for (const [key, value] of richTextComponentMap) {
|
for (const [key, value] of richTextComponentMap) {
|
||||||
if (key.startsWith('leaf.button.') || key.startsWith('leaf.component.')) {
|
if (key.startsWith('leaf.button.') || key.startsWith('leaf.component.')) {
|
||||||
const leafName = key.replace('leaf.button.', '').replace('leaf.component.', '')
|
const leafName = key.replace('leaf.button.', '').replace('leaf.component.', '')
|
||||||
|
|
||||||
if (!enabledLeaves[leafName]) {
|
if (!features.leaves[leafName]) {
|
||||||
enabledLeaves[leafName] = {
|
features.leaves[leafName] = {
|
||||||
name: leafName,
|
name: leafName,
|
||||||
Button: null,
|
Button: null,
|
||||||
Leaf: null,
|
Leaf: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.startsWith('leaf.button.')) enabledLeaves[leafName].Button = value
|
if (key.startsWith('leaf.button.')) features.leaves[leafName].Button = value
|
||||||
if (key.startsWith('leaf.component.')) enabledLeaves[leafName].Leaf = value
|
if (key.startsWith('leaf.component.')) features.leaves[leafName].Leaf = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.startsWith('element.button.') || key.startsWith('element.component.')) {
|
||||||
|
const elementName = key.replace('element.button.', '').replace('element.component.', '')
|
||||||
|
|
||||||
|
if (!features.elements[elementName]) {
|
||||||
|
features.elements[elementName] = {
|
||||||
|
name: elementName,
|
||||||
|
Button: null,
|
||||||
|
Element: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.startsWith('element.button.')) features.elements[elementName].Button = value
|
||||||
|
if (key.startsWith('element.component.')) features.elements[elementName].Element = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return enabledLeaves
|
return features
|
||||||
})
|
})
|
||||||
|
|
||||||
const elements: RichTextElement[] = defaultElements
|
|
||||||
|
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
const editorRef = useRef(null)
|
const editorRef = useRef(null)
|
||||||
const toolbarRef = useRef(null)
|
const toolbarRef = useRef(null)
|
||||||
@@ -128,65 +124,72 @@ const RichText: React.FC<
|
|||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderElement = useCallback(({ attributes, children, element }) => {
|
const renderElement = useCallback(
|
||||||
// const matchedElement = enabledElements[element.type]
|
({ attributes, children, element }) => {
|
||||||
// const Element = matchedElement?.Element
|
// return <div {...attributes}>{children}</div>
|
||||||
|
|
||||||
const attr = { ...attributes }
|
const matchedElement = elements[element.type]
|
||||||
|
const Element = matchedElement?.Element
|
||||||
|
|
||||||
// // this converts text alignment to margin when dealing with void elements
|
let attr = { ...attributes }
|
||||||
// if (element.textAlign) {
|
|
||||||
// if (element.type === 'relationship' || element.type === 'upload') {
|
|
||||||
// switch (element.textAlign) {
|
|
||||||
// case 'left':
|
|
||||||
// attr = { ...attr, style: { marginRight: 'auto' } }
|
|
||||||
// break
|
|
||||||
// case 'right':
|
|
||||||
// attr = { ...attr, style: { marginLeft: 'auto' } }
|
|
||||||
// break
|
|
||||||
// case 'center':
|
|
||||||
// attr = { ...attr, style: { marginLeft: 'auto', marginRight: 'auto' } }
|
|
||||||
// break
|
|
||||||
// default:
|
|
||||||
// attr = { ...attr, style: { textAlign: element.textAlign } }
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// } else if (element.type === 'li') {
|
|
||||||
// switch (element.textAlign) {
|
|
||||||
// case 'right':
|
|
||||||
// attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'right' } }
|
|
||||||
// break
|
|
||||||
// case 'center':
|
|
||||||
// attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'center' } }
|
|
||||||
// break
|
|
||||||
// case 'left':
|
|
||||||
// default:
|
|
||||||
// attr = { ...attr, style: { listStylePosition: 'outside', textAlign: 'left' } }
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// attr = { ...attr, style: { textAlign: element.textAlign } }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (Element) {
|
// this converts text alignment to margin when dealing with void elements
|
||||||
// const el = (
|
if (element.textAlign) {
|
||||||
// <Element
|
if (element.type === 'relationship' || element.type === 'upload') {
|
||||||
// attributes={attr}
|
switch (element.textAlign) {
|
||||||
// editorRef={editorRef}
|
case 'left':
|
||||||
// element={element}
|
attr = { ...attr, style: { marginRight: 'auto' } }
|
||||||
// fieldProps={props}
|
break
|
||||||
// path={path}
|
case 'right':
|
||||||
// >
|
attr = { ...attr, style: { marginLeft: 'auto' } }
|
||||||
// {children}
|
break
|
||||||
// </Element>
|
case 'center':
|
||||||
// )
|
attr = { ...attr, style: { marginLeft: 'auto', marginRight: 'auto' } }
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
attr = { ...attr, style: { textAlign: element.textAlign } }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if (element.type === 'li') {
|
||||||
|
switch (element.textAlign) {
|
||||||
|
case 'right':
|
||||||
|
attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'right' } }
|
||||||
|
break
|
||||||
|
case 'center':
|
||||||
|
attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'center' } }
|
||||||
|
break
|
||||||
|
case 'left':
|
||||||
|
default:
|
||||||
|
attr = { ...attr, style: { listStylePosition: 'outside', textAlign: 'left' } }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
attr = { ...attr, style: { textAlign: element.textAlign } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// return el
|
if (Element) {
|
||||||
// }
|
const el = (
|
||||||
|
<ElementProvider
|
||||||
|
attributes={attr}
|
||||||
|
childNodes={children}
|
||||||
|
editorRef={editorRef}
|
||||||
|
element={element}
|
||||||
|
fieldProps={props}
|
||||||
|
path={path}
|
||||||
|
schemaPath={schemaPath}
|
||||||
|
>
|
||||||
|
{Element}
|
||||||
|
</ElementProvider>
|
||||||
|
)
|
||||||
|
|
||||||
return <div {...attr}>{children}</div>
|
return el
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
|
return <div {...attr}>{children}</div>
|
||||||
|
},
|
||||||
|
[elements, path, props, schemaPath],
|
||||||
|
)
|
||||||
|
|
||||||
const renderLeaf = useCallback(
|
const renderLeaf = useCallback(
|
||||||
({ attributes, children, leaf }) => {
|
({ attributes, children, leaf }) => {
|
||||||
@@ -334,7 +337,7 @@ const RichText: React.FC<
|
|||||||
value={valueToRender as any[]}
|
value={valueToRender as any[]}
|
||||||
>
|
>
|
||||||
<div className={`${baseClass}__wrapper`}>
|
<div className={`${baseClass}__wrapper`}>
|
||||||
{elements?.length + Object.keys(leaves)?.length > 0 && (
|
{Object.keys(elements)?.length + Object.keys(leaves)?.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className={[`${baseClass}__toolbar`, drawerIsOpen && `${baseClass}__drawerIsOpen`]
|
className={[`${baseClass}__toolbar`, drawerIsOpen && `${baseClass}__drawerIsOpen`]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -342,20 +345,24 @@ const RichText: React.FC<
|
|||||||
ref={toolbarRef}
|
ref={toolbarRef}
|
||||||
>
|
>
|
||||||
<div className={`${baseClass}__toolbar-wrap`}>
|
<div className={`${baseClass}__toolbar-wrap`}>
|
||||||
{/* {elements.map((element, i) => {
|
{Object.values(elements).map((element, i) => {
|
||||||
let elementName: string
|
const Button = element?.Button
|
||||||
if (typeof element === 'object' && element?.name) elementName = element.name
|
|
||||||
if (typeof element === 'string') elementName = element
|
|
||||||
|
|
||||||
const elementType = enabledElements[elementName]
|
|
||||||
const Button = elementType?.Button
|
|
||||||
|
|
||||||
if (Button) {
|
if (Button) {
|
||||||
return <Button fieldProps={props} key={i} path={path} />
|
return (
|
||||||
|
<ElementButtonProvider
|
||||||
|
fieldProps={props}
|
||||||
|
key={element.name}
|
||||||
|
path={path}
|
||||||
|
schemaPath={schemaPath}
|
||||||
|
>
|
||||||
|
{Button}
|
||||||
|
</ElementButtonProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
})} */}
|
})}
|
||||||
{Object.values(leaves).map((leaf, i) => {
|
{Object.values(leaves).map((leaf, i) => {
|
||||||
const Button = leaf?.Button
|
const Button = leaf?.Button
|
||||||
|
|
||||||
@@ -363,7 +370,7 @@ const RichText: React.FC<
|
|||||||
return (
|
return (
|
||||||
<LeafButtonProvider
|
<LeafButtonProvider
|
||||||
fieldProps={props}
|
fieldProps={props}
|
||||||
key={i}
|
key={leaf.name}
|
||||||
path={path}
|
path={path}
|
||||||
schemaPath={schemaPath}
|
schemaPath={schemaPath}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { useSlate } from 'slate-react'
|
import { useSlate } from 'slate-react'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { useElement } from '../../providers/ElementProvider'
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
export const Blockquote = () => {
|
||||||
|
const { attributes, children } = useElement()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<blockquote className="rich-text-blockquote" {...attributes}>
|
||||||
|
{children}
|
||||||
|
</blockquote>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { RichTextCustomElement } from '../../..'
|
||||||
|
|
||||||
import BlockquoteIcon from '../../icons/Blockquote'
|
import BlockquoteIcon from '../../icons/Blockquote'
|
||||||
import ElementButton from '../Button'
|
import ElementButton from '../Button'
|
||||||
import './index.scss'
|
import { Blockquote } from './Blockquote'
|
||||||
|
|
||||||
const Blockquote = ({ attributes, children }) => (
|
const name = 'blockquote'
|
||||||
<blockquote className="rich-text-blockquote" {...attributes}>
|
|
||||||
{children}
|
|
||||||
</blockquote>
|
|
||||||
)
|
|
||||||
|
|
||||||
const blockquote = {
|
const blockquote: RichTextCustomElement = {
|
||||||
|
name,
|
||||||
Button: () => (
|
Button: () => (
|
||||||
<ElementButton format="blockquote">
|
<ElementButton format={name}>
|
||||||
<BlockquoteIcon />
|
<BlockquoteIcon />
|
||||||
</ElementButton>
|
</ElementButton>
|
||||||
),
|
),
|
||||||
|
|||||||
11
packages/richtext-slate/src/field/elements/h1/Heading1.tsx
Normal file
11
packages/richtext-slate/src/field/elements/h1/Heading1.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { useElement } from '../../providers/ElementProvider'
|
||||||
|
|
||||||
|
export const Heading1 = () => {
|
||||||
|
const { attributes, children } = useElement()
|
||||||
|
|
||||||
|
return <h1 {...attributes}>{children}</h1>
|
||||||
|
}
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { RichTextCustomElement } from '../../..'
|
||||||
|
|
||||||
import H1Icon from '../../icons/headings/H1'
|
import H1Icon from '../../icons/headings/H1'
|
||||||
import ElementButton from '../Button'
|
import ElementButton from '../Button'
|
||||||
|
import { Heading1 } from './Heading1'
|
||||||
|
|
||||||
const H1 = ({ attributes, children }) => <h1 {...attributes}>{children}</h1>
|
const name = 'h1'
|
||||||
|
|
||||||
const h1 = {
|
const h1: RichTextCustomElement = {
|
||||||
|
name,
|
||||||
Button: () => (
|
Button: () => (
|
||||||
<ElementButton format="h1">
|
<ElementButton format={name}>
|
||||||
<H1Icon />
|
<H1Icon />
|
||||||
</ElementButton>
|
</ElementButton>
|
||||||
),
|
),
|
||||||
Element: H1,
|
Element: Heading1,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default h1
|
export default h1
|
||||||
|
|||||||
11
packages/richtext-slate/src/field/elements/h2/Heading2.tsx
Normal file
11
packages/richtext-slate/src/field/elements/h2/Heading2.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { useElement } from '../../providers/ElementProvider'
|
||||||
|
|
||||||
|
export const Heading2 = () => {
|
||||||
|
const { attributes, children } = useElement()
|
||||||
|
|
||||||
|
return <h2 {...attributes}>{children}</h2>
|
||||||
|
}
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { RichTextCustomElement } from '../../..'
|
||||||
|
|
||||||
import H2Icon from '../../icons/headings/H2'
|
import H2Icon from '../../icons/headings/H2'
|
||||||
import ElementButton from '../Button'
|
import ElementButton from '../Button'
|
||||||
|
import { Heading2 } from './Heading2'
|
||||||
|
|
||||||
const H2 = ({ attributes, children }) => <h2 {...attributes}>{children}</h2>
|
const name = 'h2'
|
||||||
|
|
||||||
const h2 = {
|
const h2: RichTextCustomElement = {
|
||||||
|
name,
|
||||||
Button: () => (
|
Button: () => (
|
||||||
<ElementButton format="h2">
|
<ElementButton format={name}>
|
||||||
<H2Icon />
|
<H2Icon />
|
||||||
</ElementButton>
|
</ElementButton>
|
||||||
),
|
),
|
||||||
Element: H2,
|
Element: Heading2,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default h2
|
export default h2
|
||||||
|
|||||||
11
packages/richtext-slate/src/field/elements/h3/Heading3.tsx
Normal file
11
packages/richtext-slate/src/field/elements/h3/Heading3.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { useElement } from '../../providers/ElementProvider'
|
||||||
|
|
||||||
|
export const Heading3 = () => {
|
||||||
|
const { attributes, children } = useElement()
|
||||||
|
|
||||||
|
return <h3 {...attributes}>{children}</h3>
|
||||||
|
}
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { RichTextCustomElement } from '../../..'
|
||||||
|
|
||||||
import H3Icon from '../../icons/headings/H3'
|
import H3Icon from '../../icons/headings/H3'
|
||||||
import ElementButton from '../Button'
|
import ElementButton from '../Button'
|
||||||
|
import { Heading3 } from './Heading3'
|
||||||
|
|
||||||
const H3 = ({ attributes, children }) => <h3 {...attributes}>{children}</h3>
|
const name = 'h3'
|
||||||
|
|
||||||
const h3 = {
|
const h3: RichTextCustomElement = {
|
||||||
|
name,
|
||||||
Button: () => (
|
Button: () => (
|
||||||
<ElementButton format="h3">
|
<ElementButton format={name}>
|
||||||
<H3Icon />
|
<H3Icon />
|
||||||
</ElementButton>
|
</ElementButton>
|
||||||
),
|
),
|
||||||
Element: H3,
|
Element: Heading3,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default h3
|
export default h3
|
||||||
|
|||||||
11
packages/richtext-slate/src/field/elements/h4/Heading4.tsx
Normal file
11
packages/richtext-slate/src/field/elements/h4/Heading4.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { useElement } from '../../providers/ElementProvider'
|
||||||
|
|
||||||
|
export const Heading4 = () => {
|
||||||
|
const { attributes, children } = useElement()
|
||||||
|
|
||||||
|
return <h4 {...attributes}>{children}</h4>
|
||||||
|
}
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { RichTextCustomElement } from '../../..'
|
||||||
|
|
||||||
import H4Icon from '../../icons/headings/H4'
|
import H4Icon from '../../icons/headings/H4'
|
||||||
import ElementButton from '../Button'
|
import ElementButton from '../Button'
|
||||||
|
import { Heading4 } from './Heading4'
|
||||||
|
|
||||||
const H4 = ({ attributes, children }) => <h4 {...attributes}>{children}</h4>
|
const name = 'h4'
|
||||||
|
|
||||||
const h4 = {
|
const h4: RichTextCustomElement = {
|
||||||
|
name,
|
||||||
Button: () => (
|
Button: () => (
|
||||||
<ElementButton format="h4">
|
<ElementButton format={name}>
|
||||||
<H4Icon />
|
<H4Icon />
|
||||||
</ElementButton>
|
</ElementButton>
|
||||||
),
|
),
|
||||||
Element: H4,
|
Element: Heading4,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default h4
|
export default h4
|
||||||
|
|||||||
11
packages/richtext-slate/src/field/elements/h5/Heading5.tsx
Normal file
11
packages/richtext-slate/src/field/elements/h5/Heading5.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { useElement } from '../../providers/ElementProvider'
|
||||||
|
|
||||||
|
export const Heading5 = () => {
|
||||||
|
const { attributes, children } = useElement()
|
||||||
|
|
||||||
|
return <h5 {...attributes}>{children}</h5>
|
||||||
|
}
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { RichTextCustomElement } from '../../..'
|
||||||
|
|
||||||
import H5Icon from '../../icons/headings/H5'
|
import H5Icon from '../../icons/headings/H5'
|
||||||
import ElementButton from '../Button'
|
import ElementButton from '../Button'
|
||||||
|
import { Heading5 } from './Heading5'
|
||||||
|
|
||||||
const H5 = ({ attributes, children }) => <h5 {...attributes}>{children}</h5>
|
const name = 'h5'
|
||||||
|
|
||||||
const h5 = {
|
const h5: RichTextCustomElement = {
|
||||||
|
name,
|
||||||
Button: () => (
|
Button: () => (
|
||||||
<ElementButton format="h5">
|
<ElementButton format={name}>
|
||||||
<H5Icon />
|
<H5Icon />
|
||||||
</ElementButton>
|
</ElementButton>
|
||||||
),
|
),
|
||||||
Element: H5,
|
Element: Heading5,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default h5
|
export default h5
|
||||||
|
|||||||
11
packages/richtext-slate/src/field/elements/h6/Heading6.tsx
Normal file
11
packages/richtext-slate/src/field/elements/h6/Heading6.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { useElement } from '../../providers/ElementProvider'
|
||||||
|
|
||||||
|
export const Heading6 = () => {
|
||||||
|
const { attributes, children } = useElement()
|
||||||
|
|
||||||
|
return <h6 {...attributes}>{children}</h6>
|
||||||
|
}
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { RichTextCustomElement } from '../../..'
|
||||||
|
|
||||||
import H6Icon from '../../icons/headings/H6'
|
import H6Icon from '../../icons/headings/H6'
|
||||||
import ElementButton from '../Button'
|
import ElementButton from '../Button'
|
||||||
|
import { Heading6 } from './Heading6'
|
||||||
|
|
||||||
const H6 = ({ attributes, children }) => <h6 {...attributes}>{children}</h6>
|
const name = 'h6'
|
||||||
|
|
||||||
const h6 = {
|
const h6: RichTextCustomElement = {
|
||||||
|
name,
|
||||||
Button: () => (
|
Button: () => (
|
||||||
<ElementButton format="h6">
|
<ElementButton format={name}>
|
||||||
<H6Icon />
|
<H6Icon />
|
||||||
</ElementButton>
|
</ElementButton>
|
||||||
),
|
),
|
||||||
Element: H6,
|
Element: Heading6,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default h6
|
export default h6
|
||||||
|
|||||||
214
packages/richtext-slate/src/field/elements/indent/Button.tsx
Normal file
214
packages/richtext-slate/src/field/elements/indent/Button.tsx
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import { Editor, Element, Text, Transforms } from 'slate'
|
||||||
|
import { ReactEditor, useSlate } from 'slate-react'
|
||||||
|
|
||||||
|
import type { ElementNode } from '../../../types'
|
||||||
|
|
||||||
|
import { indentType } from '.'
|
||||||
|
import IndentLeft from '../../icons/IndentLeft'
|
||||||
|
import IndentRight from '../../icons/IndentRight'
|
||||||
|
import { baseClass } from '../Button'
|
||||||
|
import { getCommonBlock } from '../getCommonBlock'
|
||||||
|
import isElementActive from '../isActive'
|
||||||
|
import { isBlockElement } from '../isBlockElement'
|
||||||
|
import listTypes from '../listTypes'
|
||||||
|
import { unwrapList } from '../unwrapList'
|
||||||
|
|
||||||
|
export const IndentButton: React.FC = () => {
|
||||||
|
const editor = useSlate()
|
||||||
|
const handleIndent = useCallback(
|
||||||
|
(e, dir) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (dir === 'left') {
|
||||||
|
if (isElementActive(editor, 'li')) {
|
||||||
|
const [, listPath] = getCommonBlock(
|
||||||
|
editor,
|
||||||
|
(n) => Element.isElement(n) && listTypes.includes(n.type),
|
||||||
|
)
|
||||||
|
|
||||||
|
const matchedParentList = Editor.above(editor, {
|
||||||
|
at: listPath,
|
||||||
|
match: (n: ElementNode) => !Editor.isEditor(n) && isBlockElement(editor, n),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (matchedParentList) {
|
||||||
|
const [parentListItem, parentListItemPath] = matchedParentList
|
||||||
|
|
||||||
|
if (parentListItem.children.length > 1) {
|
||||||
|
// Remove nested list
|
||||||
|
Transforms.unwrapNodes(editor, {
|
||||||
|
at: parentListItemPath,
|
||||||
|
match: (node, path) => {
|
||||||
|
const matches =
|
||||||
|
!Editor.isEditor(node) &&
|
||||||
|
Element.isElement(node) &&
|
||||||
|
listTypes.includes(node.type) &&
|
||||||
|
path.length === parentListItemPath.length + 1
|
||||||
|
|
||||||
|
return matches
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set li type on any children that don't have a type
|
||||||
|
Transforms.setNodes(
|
||||||
|
editor,
|
||||||
|
{ type: 'li' },
|
||||||
|
{
|
||||||
|
at: parentListItemPath,
|
||||||
|
match: (node, path) => {
|
||||||
|
const matches =
|
||||||
|
!Editor.isEditor(node) &&
|
||||||
|
Element.isElement(node) &&
|
||||||
|
node.type !== 'li' &&
|
||||||
|
path.length === parentListItemPath.length + 1
|
||||||
|
|
||||||
|
return matches
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parent list item path has changed at this point
|
||||||
|
// so we need to re-fetch the parent node
|
||||||
|
const [newParentNode] = Editor.node(editor, parentListItemPath)
|
||||||
|
|
||||||
|
// If the parent node is an li,
|
||||||
|
// lift all li nodes within
|
||||||
|
if (Element.isElement(newParentNode) && newParentNode.type === 'li') {
|
||||||
|
// Lift the nested lis
|
||||||
|
Transforms.liftNodes(editor, {
|
||||||
|
at: parentListItemPath,
|
||||||
|
match: (node, path) => {
|
||||||
|
const matches =
|
||||||
|
!Editor.isEditor(node) &&
|
||||||
|
Element.isElement(node) &&
|
||||||
|
path.length === parentListItemPath.length + 1 &&
|
||||||
|
node.type === 'li'
|
||||||
|
|
||||||
|
return matches
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Transforms.unwrapNodes(editor, {
|
||||||
|
at: parentListItemPath,
|
||||||
|
match: (node, path) => {
|
||||||
|
return (
|
||||||
|
Element.isElement(node) &&
|
||||||
|
node.type === 'li' &&
|
||||||
|
path.length === parentListItemPath.length
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
Transforms.unwrapNodes(editor, {
|
||||||
|
match: (n) => Element.isElement(n) && listTypes.includes(n.type),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unwrapList(editor, listPath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Transforms.unwrapNodes(editor, {
|
||||||
|
match: (n) => Element.isElement(n) && n.type === indentType,
|
||||||
|
mode: 'lowest',
|
||||||
|
split: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dir === 'right') {
|
||||||
|
const isCurrentlyOL = isElementActive(editor, 'ol')
|
||||||
|
const isCurrentlyUL = isElementActive(editor, 'ul')
|
||||||
|
|
||||||
|
if (isCurrentlyOL || isCurrentlyUL) {
|
||||||
|
// Get the path of the first selected li -
|
||||||
|
// Multiple lis could be selected
|
||||||
|
// and the selection may start in the middle of the first li
|
||||||
|
const [[, firstSelectedLIPath]] = Array.from(
|
||||||
|
Editor.nodes(editor, {
|
||||||
|
match: (node) => Element.isElement(node) && node.type === 'li',
|
||||||
|
mode: 'lowest',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Is the first selected li the first in its list?
|
||||||
|
const hasPrecedingLI = firstSelectedLIPath[firstSelectedLIPath.length - 1] > 0
|
||||||
|
|
||||||
|
// If the first selected li is NOT the first in its list,
|
||||||
|
// we need to inject it into the prior li
|
||||||
|
if (hasPrecedingLI) {
|
||||||
|
const [, precedingLIPath] = Editor.previous(editor, {
|
||||||
|
at: firstSelectedLIPath,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [precedingLIChildren] = Editor.node(editor, [...precedingLIPath, 0])
|
||||||
|
const precedingLIChildrenIsText = Text.isText(precedingLIChildren)
|
||||||
|
|
||||||
|
if (precedingLIChildrenIsText) {
|
||||||
|
// Wrap the prior li text content so that it can be nested next to a list
|
||||||
|
Transforms.wrapNodes(editor, { children: [] }, { at: [...precedingLIPath, 0] })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the selected lis after the prior li contents
|
||||||
|
Transforms.moveNodes(editor, {
|
||||||
|
match: (node) => Element.isElement(node) && node.type === 'li',
|
||||||
|
mode: 'lowest',
|
||||||
|
to: [...precedingLIPath, 1],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wrap the selected lis in a new list
|
||||||
|
Transforms.wrapNodes(
|
||||||
|
editor,
|
||||||
|
{
|
||||||
|
children: [],
|
||||||
|
type: isCurrentlyOL ? 'ol' : 'ul',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: (node) => Element.isElement(node) && node.type === 'li',
|
||||||
|
mode: 'lowest',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Otherwise, just wrap the node in a list / li
|
||||||
|
Transforms.wrapNodes(
|
||||||
|
editor,
|
||||||
|
{
|
||||||
|
children: [{ children: [], type: 'li' }],
|
||||||
|
type: isCurrentlyOL ? 'ol' : 'ul',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: (node) => Element.isElement(node) && node.type === 'li',
|
||||||
|
mode: 'lowest',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Transforms.wrapNodes(editor, { children: [], type: indentType })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactEditor.focus(editor)
|
||||||
|
},
|
||||||
|
[editor],
|
||||||
|
)
|
||||||
|
|
||||||
|
const canDeIndent = isElementActive(editor, 'li') || isElementActive(editor, indentType)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<button
|
||||||
|
className={[baseClass, !canDeIndent && `${baseClass}--disabled`].filter(Boolean).join(' ')}
|
||||||
|
onClick={canDeIndent ? (e) => handleIndent(e, 'left') : undefined}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<IndentLeft />
|
||||||
|
</button>
|
||||||
|
<button className={baseClass} onClick={(e) => handleIndent(e, 'right')} type="button">
|
||||||
|
<IndentRight />
|
||||||
|
</button>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { useElement } from '../../providers/ElementProvider'
|
||||||
|
|
||||||
|
export const IndentElement: React.FC = () => {
|
||||||
|
const { attributes, children } = useElement()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ paddingLeft: 25 }} {...attributes}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
12
packages/richtext-slate/src/field/elements/indent/index.ts
Normal file
12
packages/richtext-slate/src/field/elements/indent/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { IndentButton } from './Button'
|
||||||
|
import { IndentElement } from './Element'
|
||||||
|
|
||||||
|
export const indentType = 'indent'
|
||||||
|
|
||||||
|
const indent = {
|
||||||
|
name: indentType,
|
||||||
|
Button: IndentButton,
|
||||||
|
Element: IndentElement,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default indent
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
import React, { useCallback } from 'react'
|
|
||||||
import { Editor, Element, Text, Transforms } from 'slate'
|
|
||||||
import { ReactEditor, useSlate } from 'slate-react'
|
|
||||||
|
|
||||||
import type { ElementNode } from '../../../types'
|
|
||||||
|
|
||||||
import IndentLeft from '../../icons/IndentLeft'
|
|
||||||
import IndentRight from '../../icons/IndentRight'
|
|
||||||
import { baseClass } from '../Button'
|
|
||||||
import { getCommonBlock } from '../getCommonBlock'
|
|
||||||
import isElementActive from '../isActive'
|
|
||||||
import { isBlockElement } from '../isBlockElement'
|
|
||||||
import listTypes from '../listTypes'
|
|
||||||
import { unwrapList } from '../unwrapList'
|
|
||||||
|
|
||||||
const indentType = 'indent'
|
|
||||||
|
|
||||||
const IndentWithPadding = ({ attributes, children }) => (
|
|
||||||
<div style={{ paddingLeft: 25 }} {...attributes}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const indent = {
|
|
||||||
Button: () => {
|
|
||||||
const editor = useSlate()
|
|
||||||
const handleIndent = useCallback(
|
|
||||||
(e, dir) => {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
if (dir === 'left') {
|
|
||||||
if (isElementActive(editor, 'li')) {
|
|
||||||
const [, listPath] = getCommonBlock(
|
|
||||||
editor,
|
|
||||||
(n) => Element.isElement(n) && listTypes.includes(n.type),
|
|
||||||
)
|
|
||||||
|
|
||||||
const matchedParentList = Editor.above(editor, {
|
|
||||||
at: listPath,
|
|
||||||
match: (n: ElementNode) => !Editor.isEditor(n) && isBlockElement(editor, n),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (matchedParentList) {
|
|
||||||
const [parentListItem, parentListItemPath] = matchedParentList
|
|
||||||
|
|
||||||
if (parentListItem.children.length > 1) {
|
|
||||||
// Remove nested list
|
|
||||||
Transforms.unwrapNodes(editor, {
|
|
||||||
at: parentListItemPath,
|
|
||||||
match: (node, path) => {
|
|
||||||
const matches =
|
|
||||||
!Editor.isEditor(node) &&
|
|
||||||
Element.isElement(node) &&
|
|
||||||
listTypes.includes(node.type) &&
|
|
||||||
path.length === parentListItemPath.length + 1
|
|
||||||
|
|
||||||
return matches
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set li type on any children that don't have a type
|
|
||||||
Transforms.setNodes(
|
|
||||||
editor,
|
|
||||||
{ type: 'li' },
|
|
||||||
{
|
|
||||||
at: parentListItemPath,
|
|
||||||
match: (node, path) => {
|
|
||||||
const matches =
|
|
||||||
!Editor.isEditor(node) &&
|
|
||||||
Element.isElement(node) &&
|
|
||||||
node.type !== 'li' &&
|
|
||||||
path.length === parentListItemPath.length + 1
|
|
||||||
|
|
||||||
return matches
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Parent list item path has changed at this point
|
|
||||||
// so we need to re-fetch the parent node
|
|
||||||
const [newParentNode] = Editor.node(editor, parentListItemPath)
|
|
||||||
|
|
||||||
// If the parent node is an li,
|
|
||||||
// lift all li nodes within
|
|
||||||
if (Element.isElement(newParentNode) && newParentNode.type === 'li') {
|
|
||||||
// Lift the nested lis
|
|
||||||
Transforms.liftNodes(editor, {
|
|
||||||
at: parentListItemPath,
|
|
||||||
match: (node, path) => {
|
|
||||||
const matches =
|
|
||||||
!Editor.isEditor(node) &&
|
|
||||||
Element.isElement(node) &&
|
|
||||||
path.length === parentListItemPath.length + 1 &&
|
|
||||||
node.type === 'li'
|
|
||||||
|
|
||||||
return matches
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Transforms.unwrapNodes(editor, {
|
|
||||||
at: parentListItemPath,
|
|
||||||
match: (node, path) => {
|
|
||||||
return (
|
|
||||||
Element.isElement(node) &&
|
|
||||||
node.type === 'li' &&
|
|
||||||
path.length === parentListItemPath.length
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
Transforms.unwrapNodes(editor, {
|
|
||||||
match: (n) => Element.isElement(n) && listTypes.includes(n.type),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unwrapList(editor, listPath)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Transforms.unwrapNodes(editor, {
|
|
||||||
match: (n) => Element.isElement(n) && n.type === indentType,
|
|
||||||
mode: 'lowest',
|
|
||||||
split: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dir === 'right') {
|
|
||||||
const isCurrentlyOL = isElementActive(editor, 'ol')
|
|
||||||
const isCurrentlyUL = isElementActive(editor, 'ul')
|
|
||||||
|
|
||||||
if (isCurrentlyOL || isCurrentlyUL) {
|
|
||||||
// Get the path of the first selected li -
|
|
||||||
// Multiple lis could be selected
|
|
||||||
// and the selection may start in the middle of the first li
|
|
||||||
const [[, firstSelectedLIPath]] = Array.from(
|
|
||||||
Editor.nodes(editor, {
|
|
||||||
match: (node) => Element.isElement(node) && node.type === 'li',
|
|
||||||
mode: 'lowest',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Is the first selected li the first in its list?
|
|
||||||
const hasPrecedingLI = firstSelectedLIPath[firstSelectedLIPath.length - 1] > 0
|
|
||||||
|
|
||||||
// If the first selected li is NOT the first in its list,
|
|
||||||
// we need to inject it into the prior li
|
|
||||||
if (hasPrecedingLI) {
|
|
||||||
const [, precedingLIPath] = Editor.previous(editor, {
|
|
||||||
at: firstSelectedLIPath,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [precedingLIChildren] = Editor.node(editor, [...precedingLIPath, 0])
|
|
||||||
const precedingLIChildrenIsText = Text.isText(precedingLIChildren)
|
|
||||||
|
|
||||||
if (precedingLIChildrenIsText) {
|
|
||||||
// Wrap the prior li text content so that it can be nested next to a list
|
|
||||||
Transforms.wrapNodes(editor, { children: [] }, { at: [...precedingLIPath, 0] })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move the selected lis after the prior li contents
|
|
||||||
Transforms.moveNodes(editor, {
|
|
||||||
match: (node) => Element.isElement(node) && node.type === 'li',
|
|
||||||
mode: 'lowest',
|
|
||||||
to: [...precedingLIPath, 1],
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wrap the selected lis in a new list
|
|
||||||
Transforms.wrapNodes(
|
|
||||||
editor,
|
|
||||||
{
|
|
||||||
children: [],
|
|
||||||
type: isCurrentlyOL ? 'ol' : 'ul',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: (node) => Element.isElement(node) && node.type === 'li',
|
|
||||||
mode: 'lowest',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Otherwise, just wrap the node in a list / li
|
|
||||||
Transforms.wrapNodes(
|
|
||||||
editor,
|
|
||||||
{
|
|
||||||
children: [{ children: [], type: 'li' }],
|
|
||||||
type: isCurrentlyOL ? 'ol' : 'ul',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: (node) => Element.isElement(node) && node.type === 'li',
|
|
||||||
mode: 'lowest',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Transforms.wrapNodes(editor, { children: [], type: indentType })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ReactEditor.focus(editor)
|
|
||||||
},
|
|
||||||
[editor],
|
|
||||||
)
|
|
||||||
|
|
||||||
const canDeIndent = isElementActive(editor, 'li') || isElementActive(editor, indentType)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<button
|
|
||||||
className={[baseClass, !canDeIndent && `${baseClass}--disabled`]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')}
|
|
||||||
onClick={canDeIndent ? (e) => handleIndent(e, 'left') : undefined}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<IndentLeft />
|
|
||||||
</button>
|
|
||||||
<button className={baseClass} onClick={(e) => handleIndent(e, 'right')} type="button">
|
|
||||||
<IndentRight />
|
|
||||||
</button>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
Element: IndentWithPadding,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default indent
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { RichTextCustomElement } from '../..'
|
||||||
|
|
||||||
import blockquote from './blockquote'
|
import blockquote from './blockquote'
|
||||||
import h1 from './h1'
|
import h1 from './h1'
|
||||||
import h2 from './h2'
|
import h2 from './h2'
|
||||||
@@ -5,16 +7,16 @@ import h3 from './h3'
|
|||||||
import h4 from './h4'
|
import h4 from './h4'
|
||||||
import h5 from './h5'
|
import h5 from './h5'
|
||||||
import h6 from './h6'
|
import h6 from './h6'
|
||||||
import indent from './indent'
|
// import indent from './indent'
|
||||||
import li from './li'
|
import li from './li'
|
||||||
import link from './link'
|
import link from './link'
|
||||||
import ol from './ol'
|
import ol from './ol'
|
||||||
import relationship from './relationship'
|
// import relationship from './relationship'
|
||||||
import textAlign from './textAlign'
|
// import textAlign from './textAlign'
|
||||||
import ul from './ul'
|
import ul from './ul'
|
||||||
import upload from './upload'
|
// import upload from './upload'
|
||||||
|
|
||||||
const elements = {
|
const elements: Record<string, RichTextCustomElement> = {
|
||||||
blockquote,
|
blockquote,
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
@@ -22,14 +24,14 @@ const elements = {
|
|||||||
h4,
|
h4,
|
||||||
h5,
|
h5,
|
||||||
h6,
|
h6,
|
||||||
indent,
|
// indent,
|
||||||
li,
|
li,
|
||||||
link,
|
link,
|
||||||
ol,
|
ol,
|
||||||
relationship,
|
// relationship,
|
||||||
textAlign,
|
// textAlign,
|
||||||
ul,
|
ul,
|
||||||
upload,
|
// upload,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default elements
|
export default elements
|
||||||
|
|||||||
30
packages/richtext-slate/src/field/elements/li/ListItem.tsx
Normal file
30
packages/richtext-slate/src/field/elements/li/ListItem.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { isValidElement } from 'react'
|
||||||
|
|
||||||
|
import { useElement } from '../../providers/ElementProvider'
|
||||||
|
import listTypes from '../listTypes'
|
||||||
|
import { Element } from 'slate'
|
||||||
|
|
||||||
|
export const ListItemElement: React.FC = () => {
|
||||||
|
const { attributes, children, element } = useElement<Element>()
|
||||||
|
|
||||||
|
if (!isValidElement(element)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const listType = typeof element.children?.[0]?.type === 'string' ? element.children[0].type : ''
|
||||||
|
const disableListStyle = element.children.length >= 1 && listTypes.includes(listType)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
style={{
|
||||||
|
listStyle: disableListStyle ? 'none' : undefined,
|
||||||
|
listStylePosition: disableListStyle ? 'outside' : undefined,
|
||||||
|
}}
|
||||||
|
{...attributes}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,25 +1,10 @@
|
|||||||
import React from 'react'
|
import type { RichTextCustomElement } from '../../..'
|
||||||
|
|
||||||
import listTypes from '../listTypes'
|
import { ListItemElement } from './ListItem'
|
||||||
|
|
||||||
const LI = (props) => {
|
const listItem: RichTextCustomElement = {
|
||||||
const { attributes, children, element } = props
|
name: 'li',
|
||||||
const disableListStyle =
|
Element: ListItemElement,
|
||||||
element.children.length >= 1 && listTypes.includes(element.children?.[0]?.type)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
style={{
|
|
||||||
listStyle: disableListStyle ? 'none' : undefined,
|
|
||||||
listStylePosition: disableListStyle ? 'outside' : undefined,
|
|
||||||
}}
|
|
||||||
{...attributes}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default listItem
|
||||||
Element: LI,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,30 +1,26 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { Fields } from '@payloadcms/ui'
|
import type { FormState } from '@payloadcms/ui'
|
||||||
|
|
||||||
import { useModal } from '@faceless-ui/modal'
|
import { useModal } from '@faceless-ui/modal'
|
||||||
import {
|
import {
|
||||||
buildStateFromSchema,
|
getFormState,
|
||||||
reduceFieldsToValues,
|
reduceFieldsToValues,
|
||||||
useAuth,
|
|
||||||
useConfig,
|
useConfig,
|
||||||
useDocumentInfo,
|
useDocumentInfo,
|
||||||
useDrawerSlug,
|
useDrawerSlug,
|
||||||
useLocale,
|
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
import { sanitizeFields } from 'payload/config'
|
|
||||||
import React, { Fragment, useState } from 'react'
|
import React, { Fragment, useState } from 'react'
|
||||||
import { Editor, Range, Transforms } from 'slate'
|
import { Editor, Range, Transforms } from 'slate'
|
||||||
import { ReactEditor, useSlate } from 'slate-react'
|
import { ReactEditor, useSlate } from 'slate-react'
|
||||||
|
|
||||||
import type { FieldProps } from '../../../../types'
|
|
||||||
|
|
||||||
import LinkIcon from '../../../icons/Link'
|
import LinkIcon from '../../../icons/Link'
|
||||||
|
import { useElementButton } from '../../../providers/ElementButtonProvider'
|
||||||
import ElementButton from '../../Button'
|
import ElementButton from '../../Button'
|
||||||
import isElementActive from '../../isActive'
|
import isElementActive from '../../isActive'
|
||||||
import { LinkDrawer } from '../LinkDrawer'
|
import { LinkDrawer } from '../LinkDrawer'
|
||||||
import { transformExtraFields, unwrapLink } from '../utilities'
|
import { unwrapLink } from '../utilities'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is called when an new link is created - not when an existing link is edited.
|
* This function is called when an new link is created - not when an existing link is edited.
|
||||||
@@ -64,35 +60,23 @@ const insertLink = (editor, fields) => {
|
|||||||
ReactEditor.focus(editor)
|
ReactEditor.focus(editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LinkButton: React.FC<{
|
export const LinkButton: React.FC = () => {
|
||||||
fieldProps: FieldProps
|
const { fieldProps } = useElementButton()
|
||||||
path: string
|
const [initialState, setInitialState] = useState<FormState>({})
|
||||||
}> = ({ fieldProps }) => {
|
|
||||||
const customFieldSchema = fieldProps?.admin?.link?.fields
|
|
||||||
const { user } = useAuth()
|
|
||||||
const { code: locale } = useLocale()
|
|
||||||
const [initialState, setInitialState] = useState<Fields>({})
|
|
||||||
|
|
||||||
const { i18n, t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const editor = useSlate()
|
const editor = useSlate()
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
|
|
||||||
const [fieldSchema] = useState(() => {
|
|
||||||
const fieldsUnsanitized = transformExtraFields(customFieldSchema, config, i18n)
|
|
||||||
// Sanitize custom fields here
|
|
||||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
|
||||||
const fields = sanitizeFields({
|
|
||||||
config: config,
|
|
||||||
fields: fieldsUnsanitized,
|
|
||||||
validRelationships,
|
|
||||||
})
|
|
||||||
|
|
||||||
return fields
|
|
||||||
})
|
|
||||||
|
|
||||||
const { closeModal, openModal } = useModal()
|
const { closeModal, openModal } = useModal()
|
||||||
const drawerSlug = useDrawerSlug('rich-text-link')
|
const drawerSlug = useDrawerSlug('rich-text-link')
|
||||||
const { getDocPreferences } = useDocumentInfo()
|
const { id, getDocPreferences } = useDocumentInfo()
|
||||||
|
|
||||||
|
const { richTextComponentMap } = fieldProps
|
||||||
|
|
||||||
|
const linkFieldsSchemaPath = `link.fields`
|
||||||
|
|
||||||
|
const fieldMap = richTextComponentMap.get(linkFieldsSchemaPath)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -104,24 +88,22 @@ export const LinkButton: React.FC<{
|
|||||||
unwrapLink(editor)
|
unwrapLink(editor)
|
||||||
} else {
|
} else {
|
||||||
openModal(drawerSlug)
|
openModal(drawerSlug)
|
||||||
|
|
||||||
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection)
|
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection)
|
||||||
|
|
||||||
if (!isCollapsed) {
|
if (!isCollapsed) {
|
||||||
const data = {
|
const data = {
|
||||||
text: editor.selection ? Editor.string(editor, editor.selection) : '',
|
text: editor.selection ? Editor.string(editor, editor.selection) : '',
|
||||||
}
|
}
|
||||||
|
const docPreferences = await getDocPreferences()
|
||||||
const preferences = await getDocPreferences()
|
const state = await getFormState({
|
||||||
const state = await buildStateFromSchema({
|
apiRoute: config.routes.api,
|
||||||
config,
|
body: {
|
||||||
data,
|
id,
|
||||||
fieldSchema,
|
data,
|
||||||
locale,
|
docPreferences,
|
||||||
operation: 'create',
|
operation: 'update',
|
||||||
preferences,
|
schemaPath: linkFieldsSchemaPath,
|
||||||
t,
|
},
|
||||||
user,
|
serverURL: config.serverURL,
|
||||||
})
|
})
|
||||||
setInitialState(state)
|
setInitialState(state)
|
||||||
}
|
}
|
||||||
@@ -133,7 +115,7 @@ export const LinkButton: React.FC<{
|
|||||||
</ElementButton>
|
</ElementButton>
|
||||||
<LinkDrawer
|
<LinkDrawer
|
||||||
drawerSlug={drawerSlug}
|
drawerSlug={drawerSlug}
|
||||||
fieldSchema={fieldSchema}
|
fieldMap={Array.isArray(fieldMap) ? fieldMap : []}
|
||||||
handleClose={() => {
|
handleClose={() => {
|
||||||
closeModal(drawerSlug)
|
closeModal(drawerSlug)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { Fields } from '@payloadcms/ui'
|
|
||||||
import type { HTMLAttributes } from 'react'
|
|
||||||
|
|
||||||
import { useModal } from '@faceless-ui/modal'
|
import { useModal } from '@faceless-ui/modal'
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
FormState,
|
||||||
Popup,
|
Popup,
|
||||||
Translation,
|
Translation,
|
||||||
buildStateFromSchema,
|
getFormState,
|
||||||
reduceFieldsToValues,
|
reduceFieldsToValues,
|
||||||
useAuth,
|
useAuth,
|
||||||
useConfig,
|
useConfig,
|
||||||
@@ -18,17 +16,16 @@ import {
|
|||||||
useLocale,
|
useLocale,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
import { sanitizeFields } from 'payload/config'
|
|
||||||
import { deepCopyObject } from 'payload/utilities'
|
import { deepCopyObject } from 'payload/utilities'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { Editor, Node, Transforms } from 'slate'
|
import { Editor, Node, Transforms } from 'slate'
|
||||||
import { ReactEditor, useSlate } from 'slate-react'
|
import { ReactEditor, useSlate } from 'slate-react'
|
||||||
|
|
||||||
import type { FieldProps } from '../../../../types'
|
import { useElement } from '../../../providers/ElementProvider'
|
||||||
|
|
||||||
import { LinkDrawer } from '../LinkDrawer'
|
import { LinkDrawer } from '../LinkDrawer'
|
||||||
import { transformExtraFields, unwrapLink } from '../utilities'
|
import { unwrapLink } from '../utilities'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
import { LinkElementType } from '../types'
|
||||||
|
|
||||||
const baseClass = 'rich-text-link'
|
const baseClass = 'rich-text-link'
|
||||||
|
|
||||||
@@ -36,7 +33,7 @@ const baseClass = 'rich-text-link'
|
|||||||
* This function is called when an existing link is edited.
|
* This function is called when an existing link is edited.
|
||||||
* When a link is first created, another function is called: {@link ../Button/index.tsx#insertLink}
|
* When a link is first created, another function is called: {@link ../Button/index.tsx#insertLink}
|
||||||
*/
|
*/
|
||||||
const insertChange = (editor, fields, customFieldSchema) => {
|
const insertChange = (editor, fields) => {
|
||||||
const data = reduceFieldsToValues(fields, true)
|
const data = reduceFieldsToValues(fields, true)
|
||||||
|
|
||||||
const [, parentPath] = Editor.above(editor)
|
const [, parentPath] = Editor.above(editor)
|
||||||
@@ -48,10 +45,6 @@ const insertChange = (editor, fields, customFieldSchema) => {
|
|||||||
url: data.url,
|
url: data.url,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customFieldSchema) {
|
|
||||||
newNode.fields = data.fields
|
|
||||||
}
|
|
||||||
|
|
||||||
Transforms.setNodes(editor, newNode, { at: parentPath })
|
Transforms.setNodes(editor, newNode, { at: parentPath })
|
||||||
|
|
||||||
Transforms.delete(editor, { at: editor.selection.focus.path, unit: 'block' })
|
Transforms.delete(editor, { at: editor.selection.focus.path, unit: 'block' })
|
||||||
@@ -61,16 +54,14 @@ const insertChange = (editor, fields, customFieldSchema) => {
|
|||||||
ReactEditor.focus(editor)
|
ReactEditor.focus(editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LinkElement: React.FC<{
|
export const LinkElement = () => {
|
||||||
attributes: HTMLAttributes<HTMLDivElement>
|
const { attributes, children, editorRef, element, fieldProps, schemaPath } =
|
||||||
children: React.ReactNode
|
useElement<LinkElementType>()
|
||||||
editorRef: React.RefObject<HTMLDivElement>
|
|
||||||
element: any
|
|
||||||
fieldProps: FieldProps
|
|
||||||
}> = (props) => {
|
|
||||||
const { attributes, children, editorRef, element, fieldProps } = props
|
|
||||||
|
|
||||||
const customFieldSchema = fieldProps?.admin?.link?.fields
|
const linkFieldsSchemaPath = `${schemaPath}.link.fields`
|
||||||
|
|
||||||
|
const { richTextComponentMap } = fieldProps
|
||||||
|
const fieldMap = richTextComponentMap.get(linkFieldsSchemaPath)
|
||||||
|
|
||||||
const editor = useSlate()
|
const editor = useSlate()
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
@@ -80,20 +71,8 @@ export const LinkElement: React.FC<{
|
|||||||
const { closeModal, openModal, toggleModal } = useModal()
|
const { closeModal, openModal, toggleModal } = useModal()
|
||||||
const [renderModal, setRenderModal] = useState(false)
|
const [renderModal, setRenderModal] = useState(false)
|
||||||
const [renderPopup, setRenderPopup] = useState(false)
|
const [renderPopup, setRenderPopup] = useState(false)
|
||||||
const [initialState, setInitialState] = useState<Fields>({})
|
const [initialState, setInitialState] = useState<FormState>({})
|
||||||
const { getDocPreferences } = useDocumentInfo()
|
const { id, getDocPreferences } = useDocumentInfo()
|
||||||
const [fieldSchema] = useState(() => {
|
|
||||||
const fieldsUnsanitized = transformExtraFields(customFieldSchema, config, i18n)
|
|
||||||
// Sanitize custom fields here
|
|
||||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
|
||||||
const fields = sanitizeFields({
|
|
||||||
config: config,
|
|
||||||
fields: fieldsUnsanitized,
|
|
||||||
validRelationships,
|
|
||||||
})
|
|
||||||
|
|
||||||
return fields
|
|
||||||
})
|
|
||||||
|
|
||||||
const drawerSlug = useDrawerSlug('rich-text-link')
|
const drawerSlug = useDrawerSlug('rich-text-link')
|
||||||
|
|
||||||
@@ -114,22 +93,25 @@ export const LinkElement: React.FC<{
|
|||||||
url: element.url,
|
url: element.url,
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferences = await getDocPreferences()
|
const docPreferences = await getDocPreferences()
|
||||||
const state = await buildStateFromSchema({
|
|
||||||
config,
|
const state = await getFormState({
|
||||||
data,
|
apiRoute: config.routes.api,
|
||||||
fieldSchema,
|
body: {
|
||||||
locale,
|
id,
|
||||||
operation: 'update',
|
data,
|
||||||
preferences,
|
docPreferences,
|
||||||
t,
|
operation: 'update',
|
||||||
user,
|
schemaPath: linkFieldsSchemaPath,
|
||||||
|
},
|
||||||
|
serverURL: config.serverURL,
|
||||||
})
|
})
|
||||||
|
|
||||||
setInitialState(state)
|
setInitialState(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
awaitInitialState()
|
awaitInitialState()
|
||||||
}, [renderModal, element, fieldSchema, user, locale, t, getDocPreferences, config])
|
}, [renderModal, element, user, locale, t, getDocPreferences, config])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={baseClass} {...attributes}>
|
<span className={baseClass} {...attributes}>
|
||||||
@@ -137,13 +119,13 @@ export const LinkElement: React.FC<{
|
|||||||
{renderModal && (
|
{renderModal && (
|
||||||
<LinkDrawer
|
<LinkDrawer
|
||||||
drawerSlug={drawerSlug}
|
drawerSlug={drawerSlug}
|
||||||
fieldSchema={fieldSchema}
|
fieldMap={Array.isArray(fieldMap) ? fieldMap : []}
|
||||||
handleClose={() => {
|
handleClose={() => {
|
||||||
toggleModal(drawerSlug)
|
toggleModal(drawerSlug)
|
||||||
setRenderModal(false)
|
setRenderModal(false)
|
||||||
}}
|
}}
|
||||||
handleModalSubmit={(fields) => {
|
handleModalSubmit={(fields) => {
|
||||||
insertChange(editor, fields, customFieldSchema)
|
insertChange(editor, fields)
|
||||||
closeModal(drawerSlug)
|
closeModal(drawerSlug)
|
||||||
}}
|
}}
|
||||||
initialState={initialState}
|
initialState={initialState}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
Form,
|
Form,
|
||||||
FormSubmit,
|
FormSubmit,
|
||||||
RenderFields,
|
RenderFields,
|
||||||
fieldTypes,
|
|
||||||
useEditDepth,
|
useEditDepth,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
@@ -20,7 +19,7 @@ const baseClass = 'rich-text-link-edit-modal'
|
|||||||
|
|
||||||
export const LinkDrawer: React.FC<Props> = ({
|
export const LinkDrawer: React.FC<Props> = ({
|
||||||
drawerSlug,
|
drawerSlug,
|
||||||
fieldSchema,
|
fieldMap,
|
||||||
handleModalSubmit,
|
handleModalSubmit,
|
||||||
initialState,
|
initialState,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -28,13 +27,8 @@ export const LinkDrawer: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer className={baseClass} slug={drawerSlug} title={t('fields:editLink')}>
|
<Drawer className={baseClass} slug={drawerSlug} title={t('fields:editLink')}>
|
||||||
<Form fields={fieldSchema} initialState={initialState} onSubmit={handleModalSubmit}>
|
<Form initialState={initialState} onSubmit={handleModalSubmit}>
|
||||||
<RenderFields
|
<RenderFields fieldMap={fieldMap} forceRender readOnly={false} />
|
||||||
fieldSchema={fieldSchema}
|
|
||||||
fieldTypes={fieldTypes}
|
|
||||||
forceRender
|
|
||||||
readOnly={false}
|
|
||||||
/>
|
|
||||||
<LinkSubmit />
|
<LinkSubmit />
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { Field, Fields } from 'payload/types'
|
import type { FieldMap, FormState } from '@payloadcms/ui'
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
drawerSlug: string
|
drawerSlug: string
|
||||||
fieldSchema: Field[]
|
fieldMap: FieldMap
|
||||||
handleClose: () => void
|
handleClose: () => void
|
||||||
handleModalSubmit: (fields: Fields, data: Record<string, unknown>) => void
|
handleModalSubmit: (fields: FormState, data: Record<string, unknown>) => void
|
||||||
initialState?: Fields
|
initialState?: FormState
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import type { RichTextCustomElement } from '../../..'
|
||||||
|
|
||||||
import { LinkButton } from './Button'
|
import { LinkButton } from './Button'
|
||||||
import { LinkElement } from './Element'
|
import { LinkElement } from './Element'
|
||||||
import { withLinks } from './utilities'
|
import { withLinks } from './utilities'
|
||||||
|
|
||||||
const link = {
|
const link: RichTextCustomElement = {
|
||||||
|
name: 'link',
|
||||||
Button: LinkButton,
|
Button: LinkButton,
|
||||||
Element: LinkElement,
|
Element: LinkElement,
|
||||||
plugins: [withLinks],
|
plugins: [withLinks],
|
||||||
|
|||||||
9
packages/richtext-slate/src/field/elements/link/types.ts
Normal file
9
packages/richtext-slate/src/field/elements/link/types.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Element } from 'slate'
|
||||||
|
|
||||||
|
export type LinkElementType = Element & {
|
||||||
|
doc: Record<string, unknown>
|
||||||
|
fields: Record<string, unknown>
|
||||||
|
linkType: string
|
||||||
|
newTab: boolean
|
||||||
|
url: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { useElement } from '../../providers/ElementProvider'
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
export const OrderedList: React.FC = () => {
|
||||||
|
const { attributes, children } = useElement()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ol className="rich-text-ol" {...attributes}>
|
||||||
|
{children}
|
||||||
|
</ol>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,22 +1,21 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { RichTextCustomElement } from '../../..'
|
||||||
|
|
||||||
import OLIcon from '../../icons/OrderedList'
|
import OLIcon from '../../icons/OrderedList'
|
||||||
import ListButton from '../ListButton'
|
import ListButton from '../ListButton'
|
||||||
import './index.scss'
|
import { OrderedList } from './OrderedList'
|
||||||
|
|
||||||
const OL = ({ attributes, children }) => (
|
const name = 'ol'
|
||||||
<ol className="rich-text-ol" {...attributes}>
|
|
||||||
{children}
|
|
||||||
</ol>
|
|
||||||
)
|
|
||||||
|
|
||||||
const ol = {
|
const ol: RichTextCustomElement = {
|
||||||
|
name,
|
||||||
Button: () => (
|
Button: () => (
|
||||||
<ListButton format="ol">
|
<ListButton format={name}>
|
||||||
<OLIcon />
|
<OLIcon />
|
||||||
</ListButton>
|
</ListButton>
|
||||||
),
|
),
|
||||||
Element: OL,
|
Element: OrderedList,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ol
|
export default ol
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { useElement } from '../../providers/ElementProvider'
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
export const UnorderedList: React.FC = () => {
|
||||||
|
const { attributes, children } = useElement()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="rich-text-ul" {...attributes}>
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,22 +1,21 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { RichTextCustomElement } from '../../..'
|
||||||
|
|
||||||
import ULIcon from '../../icons/UnorderedList'
|
import ULIcon from '../../icons/UnorderedList'
|
||||||
import ListButton from '../ListButton'
|
import ListButton from '../ListButton'
|
||||||
import './index.scss'
|
import { UnorderedList } from './UnorderedList'
|
||||||
|
|
||||||
const UL = ({ attributes, children }) => (
|
const name = 'ul'
|
||||||
<ul className="rich-text-ul" {...attributes}>
|
|
||||||
{children}
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
|
|
||||||
const ul = {
|
const ul: RichTextCustomElement = {
|
||||||
|
name,
|
||||||
Button: () => (
|
Button: () => (
|
||||||
<ListButton format="ul">
|
<ListButton format={name}>
|
||||||
<ULIcon />
|
<ULIcon />
|
||||||
</ListButton>
|
</ListButton>
|
||||||
),
|
),
|
||||||
Element: UL,
|
Element: UnorderedList,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ul
|
export default ul
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { FormFieldBase } from '../../../../ui/src/forms/fields/shared'
|
||||||
|
|
||||||
|
type ElementButtonContextType = {
|
||||||
|
fieldProps: FormFieldBase & {
|
||||||
|
name: string
|
||||||
|
richTextComponentMap: Map<string, React.ReactNode>
|
||||||
|
}
|
||||||
|
path: string
|
||||||
|
schemaPath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ElementButtonContext = React.createContext<ElementButtonContextType>({
|
||||||
|
fieldProps: {} as any,
|
||||||
|
path: '',
|
||||||
|
schemaPath: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ElementButtonProvider: React.FC<
|
||||||
|
ElementButtonContextType & {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
> = (props) => {
|
||||||
|
const { children, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ElementButtonContext.Provider
|
||||||
|
value={{
|
||||||
|
...rest,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ElementButtonContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useElementButton = () => {
|
||||||
|
const path = React.useContext(ElementButtonContext)
|
||||||
|
return path
|
||||||
|
}
|
||||||
@@ -1,27 +1,45 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
import type { Element } from 'slate'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
type ElementContextType = {
|
import type { FormFieldBase } from '../../../../ui/src/forms/fields/shared'
|
||||||
|
|
||||||
|
type ElementContextType<T> = {
|
||||||
|
attributes: Record<string, unknown>
|
||||||
|
children: React.ReactNode
|
||||||
|
editorRef: React.MutableRefObject<HTMLDivElement>
|
||||||
|
element: T
|
||||||
|
fieldProps: FormFieldBase & {
|
||||||
|
name: string
|
||||||
|
richTextComponentMap: Map<string, React.ReactNode>
|
||||||
|
}
|
||||||
path: string
|
path: string
|
||||||
schemaPath: string
|
schemaPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ElementContext = React.createContext<ElementContextType>({
|
const ElementContext = React.createContext<ElementContextType<Element>>({
|
||||||
|
attributes: {},
|
||||||
|
children: null,
|
||||||
|
editorRef: null,
|
||||||
|
element: {} as Element,
|
||||||
|
fieldProps: {} as any,
|
||||||
path: '',
|
path: '',
|
||||||
schemaPath: '',
|
schemaPath: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ElementProvider: React.FC<{
|
export const ElementProvider: React.FC<
|
||||||
children: React.ReactNode
|
ElementContextType<Element> & {
|
||||||
path: string
|
childNodes: React.ReactNode
|
||||||
schemaPath: string
|
}
|
||||||
}> = (props) => {
|
> = (props) => {
|
||||||
const { children, ...rest } = props
|
const { childNodes, children, ...rest } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ElementContext.Provider
|
<ElementContext.Provider
|
||||||
value={{
|
value={{
|
||||||
...rest,
|
...rest,
|
||||||
|
children: childNodes,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -29,7 +47,6 @@ export const ElementProvider: React.FC<{
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useElement = () => {
|
export const useElement = <T,>(): ElementContextType<T> => {
|
||||||
const path = React.useContext(ElementContext)
|
return React.useContext(ElementContext) as ElementContextType<T>
|
||||||
return path
|
|
||||||
}
|
}
|
||||||
|
|||||||
16
packages/richtext-slate/src/field/types.ts
Normal file
16
packages/richtext-slate/src/field/types.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export type EnabledFeatures = {
|
||||||
|
elements: {
|
||||||
|
[name: string]: {
|
||||||
|
Button: React.ReactNode
|
||||||
|
Element: React.ReactNode
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
leaves: {
|
||||||
|
[name: string]: {
|
||||||
|
Button: React.ReactNode
|
||||||
|
Leaf: React.ReactNode
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
import type { RichTextAdapter } from 'payload/types'
|
import type { Field, RichTextAdapter } from 'payload/types'
|
||||||
|
|
||||||
import { withMergedProps } from '@payloadcms/ui/utilities'
|
import { initI18n } from '@payloadcms/translations'
|
||||||
|
import { translations } from '@payloadcms/translations/client'
|
||||||
|
import { mapFields, withMergedProps } from '@payloadcms/ui/utilities'
|
||||||
|
import { sanitizeFields } from 'payload/config'
|
||||||
import { withNullableJSONSchemaType } from 'payload/utilities'
|
import { withNullableJSONSchemaType } from 'payload/utilities'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { AdapterArguments, RichTextCustomLeaf } from './types'
|
import type { AdapterArguments, RichTextCustomElement, RichTextCustomLeaf } from './types'
|
||||||
|
|
||||||
import RichTextCell from './cell'
|
import RichTextCell from './cell'
|
||||||
import { richTextRelationshipPromise } from './data/richTextRelationshipPromise'
|
import { richTextRelationshipPromise } from './data/richTextRelationshipPromise'
|
||||||
import { richTextValidate } from './data/validation'
|
import { richTextValidate } from './data/validation'
|
||||||
import RichTextField from './field'
|
import RichTextField from './field'
|
||||||
|
import elementTypes from './field/elements'
|
||||||
|
import { transformExtraFields } from './field/elements/link/utilities'
|
||||||
import leafTypes from './field/leaves'
|
import leafTypes from './field/leaves'
|
||||||
|
|
||||||
export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], AdapterArguments, any> {
|
export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], AdapterArguments, any> {
|
||||||
@@ -22,9 +27,12 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
|
|||||||
Component: RichTextField,
|
Component: RichTextField,
|
||||||
toMergeIntoProps: args,
|
toMergeIntoProps: args,
|
||||||
}),
|
}),
|
||||||
generateComponentMap: () => {
|
generateComponentMap: ({ config }) => {
|
||||||
const componentMap = new Map()
|
const componentMap = new Map()
|
||||||
|
|
||||||
|
const i18n = initI18n({ config: config.i18n, context: 'client', translations })
|
||||||
|
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||||
|
|
||||||
;(args?.admin?.leaves || Object.values(leafTypes)).forEach((leaf) => {
|
;(args?.admin?.leaves || Object.values(leafTypes)).forEach((leaf) => {
|
||||||
let leafObject: RichTextCustomLeaf
|
let leafObject: RichTextCustomLeaf
|
||||||
|
|
||||||
@@ -42,9 +50,92 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
|
|||||||
componentMap.set(`leaf.component.${leafObject.name}`, <LeafComponent />)
|
componentMap.set(`leaf.component.${leafObject.name}`, <LeafComponent />)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
;(args?.admin?.elements || Object.values(elementTypes)).forEach((el) => {
|
||||||
|
let element: RichTextCustomElement
|
||||||
|
|
||||||
|
if (typeof el === 'object' && el !== null) {
|
||||||
|
element = el
|
||||||
|
} else if (typeof el === 'string' && elementTypes[el]) {
|
||||||
|
element = elementTypes[el]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
const ElementButton = element.Button
|
||||||
|
const ElementComponent = element.Element
|
||||||
|
|
||||||
|
if (ElementButton) componentMap.set(`element.button.${element.name}`, <ElementButton />)
|
||||||
|
componentMap.set(`element.component.${element.name}`, <ElementComponent />)
|
||||||
|
|
||||||
|
switch (element.name) {
|
||||||
|
case 'link': {
|
||||||
|
const linkFields = sanitizeFields({
|
||||||
|
config: config,
|
||||||
|
fields: transformExtraFields(args.admin?.link?.fields, config, i18n),
|
||||||
|
validRelationships,
|
||||||
|
})
|
||||||
|
|
||||||
|
const mappedFields = mapFields({
|
||||||
|
config,
|
||||||
|
fieldSchema: linkFields,
|
||||||
|
operation: 'update',
|
||||||
|
permissions: {},
|
||||||
|
readOnly: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
componentMap.set('link.fields', mappedFields)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'upload':
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'relationship':
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return componentMap
|
return componentMap
|
||||||
},
|
},
|
||||||
|
generateSchemaMap: ({ config, schemaMap, schemaPath }) => {
|
||||||
|
const i18n = initI18n({ config: config.i18n, context: 'client', translations })
|
||||||
|
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||||
|
|
||||||
|
;(args?.admin?.elements || Object.values(elementTypes)).forEach((el) => {
|
||||||
|
let element: RichTextCustomElement
|
||||||
|
|
||||||
|
if (typeof el === 'object' && el !== null) {
|
||||||
|
element = el
|
||||||
|
} else if (typeof el === 'string' && elementTypes[el]) {
|
||||||
|
element = elementTypes[el]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
switch (element.name) {
|
||||||
|
case 'link': {
|
||||||
|
const linkFields = sanitizeFields({
|
||||||
|
config: config,
|
||||||
|
fields: transformExtraFields(args.admin?.link?.fields, config, i18n),
|
||||||
|
validRelationships,
|
||||||
|
})
|
||||||
|
|
||||||
|
schemaMap.set(`${schemaPath}.link`, linkFields)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'upload':
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'relationship':
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return schemaMap
|
||||||
|
},
|
||||||
outputSchema: ({ isRequired }) => {
|
outputSchema: ({ isRequired }) => {
|
||||||
return {
|
return {
|
||||||
items: {
|
items: {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function nodeIsTextNode(node: ElementNode | TextNode): node is TextNode {
|
|||||||
type RichTextPlugin = (editor: Editor) => Editor
|
type RichTextPlugin = (editor: Editor) => Editor
|
||||||
|
|
||||||
export type RichTextCustomElement = {
|
export type RichTextCustomElement = {
|
||||||
Button: React.ComponentType<any>
|
Button?: React.ComponentType<any>
|
||||||
Element: React.ComponentType<any>
|
Element: React.ComponentType<any>
|
||||||
name: string
|
name: string
|
||||||
plugins?: RichTextPlugin[]
|
plugins?: RichTextPlugin[]
|
||||||
|
|||||||
@@ -27,5 +27,5 @@
|
|||||||
"src/**/*.json",
|
"src/**/*.json",
|
||||||
"src/field/leaves/italic/Italic"
|
"src/field/leaves/italic/Italic"
|
||||||
],
|
],
|
||||||
"references": [{ "path": "../payload" }] // db-mongodb depends on payload
|
"references": [{ "path": "../payload" }, { "path": "../translations" }, { "path": "../ui" }] // db-mongodb depends on payload
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import type { EditViewProps } from '../../views/types'
|
|||||||
import { DefaultEditView } from '../../views/Edit'
|
import { DefaultEditView } from '../../views/Edit'
|
||||||
import { Gutter } from '../Gutter'
|
import { Gutter } from '../Gutter'
|
||||||
import { LoadingOverlay } from '../Loading'
|
import { LoadingOverlay } from '../Loading'
|
||||||
import { getFormState } from '../../views/Edit/getFormState'
|
import { getFormState } from '../../utilities/getFormState'
|
||||||
import { useFieldPath } from '../../forms/FieldPathProvider'
|
import { useFieldPath } from '../../forms/FieldPathProvider'
|
||||||
|
|
||||||
const Content: React.FC<DocumentDrawerProps> = ({ collectionSlug, Header, drawerSlug, onSave }) => {
|
const Content: React.FC<DocumentDrawerProps> = ({ collectionSlug, Header, drawerSlug, onSave }) => {
|
||||||
|
|||||||
@@ -7,3 +7,5 @@ export { withMergedProps } from '../utilities/withMergedProps'
|
|||||||
export type { FieldMap, MappedField } from '../utilities/buildComponentMap/types'
|
export type { FieldMap, MappedField } from '../utilities/buildComponentMap/types'
|
||||||
export { buildFieldSchemaMap } from '../utilities/buildFieldSchemaMap'
|
export { buildFieldSchemaMap } from '../utilities/buildFieldSchemaMap'
|
||||||
export type { FieldSchemaMap } from '../utilities/buildFieldSchemaMap/types'
|
export type { FieldSchemaMap } from '../utilities/buildFieldSchemaMap/types'
|
||||||
|
export { mapFields } from '../utilities/buildComponentMap/mapFields'
|
||||||
|
export { getFormState } from '../utilities/getFormState'
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
className,
|
className,
|
||||||
disableSuccessStatus,
|
disableSuccessStatus,
|
||||||
disabled,
|
disabled,
|
||||||
fields: fieldsFromProps,
|
|
||||||
// fields: fieldsFromProps = collection?.fields || global?.fields,
|
// fields: fieldsFromProps = collection?.fields || global?.fields,
|
||||||
handleResponse,
|
handleResponse,
|
||||||
initialState, // fully formed initial field state
|
initialState, // fully formed initial field state
|
||||||
|
|||||||
@@ -15,7 +15,12 @@ import {
|
|||||||
} from 'payload/types'
|
} from 'payload/types'
|
||||||
import { Option } from 'payload/types'
|
import { Option } from 'payload/types'
|
||||||
import { FormState } from '../..'
|
import { FormState } from '../..'
|
||||||
import type { FieldMap, ReducedBlock, MappedTab } from '../../utilities/buildComponentMap/types'
|
import type {
|
||||||
|
FieldMap,
|
||||||
|
ReducedBlock,
|
||||||
|
MappedTab,
|
||||||
|
MappedField,
|
||||||
|
} from '../../utilities/buildComponentMap/types'
|
||||||
|
|
||||||
export const fieldBaseClass = 'field-type'
|
export const fieldBaseClass = 'field-type'
|
||||||
|
|
||||||
@@ -111,7 +116,7 @@ export type FormFieldBase = {
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
// For `richText` fields
|
// For `richText` fields
|
||||||
richTextComponentMap?: Map<string, React.ReactNode>
|
richTextComponentMap?: Map<string, React.ReactNode | MappedField[]>
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export type BuildFormStateArgs = {
|
|||||||
operation?: 'create' | 'update'
|
operation?: 'create' | 'update'
|
||||||
docPreferences: DocumentPreferences
|
docPreferences: DocumentPreferences
|
||||||
formState?: FormState
|
formState?: FormState
|
||||||
|
data?: Record<string, unknown>
|
||||||
schemaPath: string
|
schemaPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const buildComponentMap = (args: {
|
|||||||
afterListTable?.map((Component) => <Component />)
|
afterListTable?.map((Component) => <Component />)
|
||||||
|
|
||||||
const mappedFields = mapFields({
|
const mappedFields = mapFields({
|
||||||
|
config,
|
||||||
fieldSchema: fields,
|
fieldSchema: fields,
|
||||||
operation,
|
operation,
|
||||||
permissions,
|
permissions,
|
||||||
@@ -70,6 +71,7 @@ export const buildComponentMap = (args: {
|
|||||||
const { fields, slug } = globalConfig
|
const { fields, slug } = globalConfig
|
||||||
|
|
||||||
const mappedFields = mapFields({
|
const mappedFields = mapFields({
|
||||||
|
config,
|
||||||
fieldSchema: fields,
|
fieldSchema: fields,
|
||||||
operation,
|
operation,
|
||||||
permissions,
|
permissions,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
import type { FieldPermissions } from 'payload/auth'
|
import type { FieldPermissions } from 'payload/auth'
|
||||||
import type { CellProps, Field, FieldWithPath, LabelProps } from 'payload/types'
|
import type { CellProps, Field, FieldWithPath, LabelProps, SanitizedConfig } from 'payload/types'
|
||||||
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/types'
|
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/types'
|
||||||
import { fieldTypes } from '../../forms/fields'
|
import { fieldTypes } from '../../forms/fields'
|
||||||
import { FormFieldBase } from '../../forms/fields/shared'
|
import { FormFieldBase } from '../../forms/fields/shared'
|
||||||
@@ -14,9 +14,9 @@ import DefaultLabel from '../../forms/Label'
|
|||||||
import DefaultError from '../../forms/Error'
|
import DefaultError from '../../forms/Error'
|
||||||
import DefaultDescription from '../../forms/FieldDescription'
|
import DefaultDescription from '../../forms/FieldDescription'
|
||||||
import { HiddenInput } from '../..'
|
import { HiddenInput } from '../..'
|
||||||
import { richText } from 'payload/fields/validations'
|
|
||||||
|
|
||||||
export const mapFields = (args: {
|
export const mapFields = (args: {
|
||||||
|
config: SanitizedConfig
|
||||||
fieldSchema: FieldWithPath[]
|
fieldSchema: FieldWithPath[]
|
||||||
filter?: (field: Field) => boolean
|
filter?: (field: Field) => boolean
|
||||||
operation?: 'create' | 'update'
|
operation?: 'create' | 'update'
|
||||||
@@ -29,6 +29,7 @@ export const mapFields = (args: {
|
|||||||
parentPath?: string
|
parentPath?: string
|
||||||
}): FieldMap => {
|
}): FieldMap => {
|
||||||
const {
|
const {
|
||||||
|
config,
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
filter,
|
filter,
|
||||||
operation = 'update',
|
operation = 'update',
|
||||||
@@ -95,6 +96,7 @@ export const mapFields = (args: {
|
|||||||
field.fields &&
|
field.fields &&
|
||||||
Array.isArray(field.fields) &&
|
Array.isArray(field.fields) &&
|
||||||
mapFields({
|
mapFields({
|
||||||
|
config,
|
||||||
fieldSchema: field.fields,
|
fieldSchema: field.fields,
|
||||||
filter,
|
filter,
|
||||||
operation,
|
operation,
|
||||||
@@ -110,6 +112,7 @@ export const mapFields = (args: {
|
|||||||
Array.isArray(field.tabs) &&
|
Array.isArray(field.tabs) &&
|
||||||
field.tabs.map((tab) => {
|
field.tabs.map((tab) => {
|
||||||
const tabFieldMap = mapFields({
|
const tabFieldMap = mapFields({
|
||||||
|
config,
|
||||||
fieldSchema: tab.fields,
|
fieldSchema: tab.fields,
|
||||||
filter,
|
filter,
|
||||||
operation,
|
operation,
|
||||||
@@ -134,12 +137,13 @@ export const mapFields = (args: {
|
|||||||
Array.isArray(field.blocks) &&
|
Array.isArray(field.blocks) &&
|
||||||
field.blocks.map((block) => {
|
field.blocks.map((block) => {
|
||||||
const blockFieldMap = mapFields({
|
const blockFieldMap = mapFields({
|
||||||
|
config,
|
||||||
fieldSchema: block.fields,
|
fieldSchema: block.fields,
|
||||||
filter,
|
filter,
|
||||||
operation,
|
operation,
|
||||||
permissions,
|
permissions,
|
||||||
readOnly: readOnlyOverride,
|
readOnly: readOnlyOverride,
|
||||||
parentPath: path,
|
parentPath: `${path}.${block.slug}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
const reducedBlock: ReducedBlock = {
|
const reducedBlock: ReducedBlock = {
|
||||||
@@ -267,7 +271,7 @@ export const mapFields = (args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof field.editor.generateComponentMap === 'function') {
|
if (typeof field.editor.generateComponentMap === 'function') {
|
||||||
const result = field.editor.generateComponentMap()
|
const result = field.editor.generateComponentMap({ config, schemaPath: path })
|
||||||
// @ts-ignore-next-line // TODO: the `richTextComponentMap` is not found on the union type
|
// @ts-ignore-next-line // TODO: the `richTextComponentMap` is not found on the union type
|
||||||
fieldComponentProps.richTextComponentMap = result
|
fieldComponentProps.richTextComponentMap = result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const buildFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap =>
|
|||||||
|
|
||||||
config.collections.forEach((collection) => {
|
config.collections.forEach((collection) => {
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
config,
|
||||||
schemaPath: collection.slug,
|
schemaPath: collection.slug,
|
||||||
fields: collection.fields,
|
fields: collection.fields,
|
||||||
schemaMap: result,
|
schemaMap: result,
|
||||||
@@ -15,6 +16,7 @@ export const buildFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap =>
|
|||||||
|
|
||||||
config.globals.forEach((global) => {
|
config.globals.forEach((global) => {
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
config,
|
||||||
schemaPath: global.slug,
|
schemaPath: global.slug,
|
||||||
fields: global.fields,
|
fields: global.fields,
|
||||||
schemaMap: result,
|
schemaMap: result,
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { Field, tabHasName } from 'payload/types'
|
import { Field, SanitizedConfig, tabHasName } from 'payload/types'
|
||||||
import { FieldSchemaMap } from './types'
|
import { FieldSchemaMap } from './types'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
|
config: SanitizedConfig
|
||||||
fields: Field[]
|
fields: Field[]
|
||||||
schemaMap: FieldSchemaMap
|
schemaMap: FieldSchemaMap
|
||||||
schemaPath: string
|
schemaPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const traverseFields = ({ fields, schemaMap, schemaPath }: Args) => {
|
export const traverseFields = ({ config, fields, schemaMap, schemaPath }: Args) => {
|
||||||
fields.map((field) => {
|
fields.map((field) => {
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case 'group':
|
case 'group':
|
||||||
case 'array':
|
case 'array':
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
config,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
schemaMap,
|
schemaMap,
|
||||||
schemaPath: `${schemaPath}.${field.name}`,
|
schemaPath: `${schemaPath}.${field.name}`,
|
||||||
@@ -22,6 +24,7 @@ export const traverseFields = ({ fields, schemaMap, schemaPath }: Args) => {
|
|||||||
case 'collapsible':
|
case 'collapsible':
|
||||||
case 'row':
|
case 'row':
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
config,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
schemaMap,
|
schemaMap,
|
||||||
schemaPath,
|
schemaPath,
|
||||||
@@ -31,6 +34,7 @@ export const traverseFields = ({ fields, schemaMap, schemaPath }: Args) => {
|
|||||||
case 'blocks':
|
case 'blocks':
|
||||||
field.blocks.map((block) => {
|
field.blocks.map((block) => {
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
config,
|
||||||
fields: block.fields,
|
fields: block.fields,
|
||||||
schemaMap,
|
schemaMap,
|
||||||
schemaPath: `${schemaPath}.${field.name}.${block.slug}`,
|
schemaPath: `${schemaPath}.${field.name}.${block.slug}`,
|
||||||
@@ -38,10 +42,18 @@ export const traverseFields = ({ fields, schemaMap, schemaPath }: Args) => {
|
|||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'richText':
|
||||||
|
if (typeof field.editor.generateSchemaMap === 'function') {
|
||||||
|
field.editor.generateSchemaMap({ schemaPath, config, schemaMap })
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
case 'tabs':
|
case 'tabs':
|
||||||
field.tabs.map((tab) => {
|
field.tabs.map((tab) => {
|
||||||
const tabSchemaPath = tabHasName(tab) ? `${schemaPath}.${tab.name}` : schemaPath
|
const tabSchemaPath = tabHasName(tab) ? `${schemaPath}.${tab.name}` : schemaPath
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
config,
|
||||||
fields: tab.fields,
|
fields: tab.fields,
|
||||||
schemaMap,
|
schemaMap,
|
||||||
schemaPath: tabSchemaPath,
|
schemaPath: tabSchemaPath,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SanitizedConfig } from 'payload/types'
|
import { SanitizedConfig } from 'payload/types'
|
||||||
import { FormState } from '../../forms/Form/types'
|
import { FormState } from '../forms/Form/types'
|
||||||
import { BuildFormStateArgs } from '../..'
|
import { BuildFormStateArgs } from '..'
|
||||||
|
|
||||||
export const getFormState = async (args: {
|
export const getFormState = async (args: {
|
||||||
serverURL: SanitizedConfig['serverURL']
|
serverURL: SanitizedConfig['serverURL']
|
||||||
@@ -20,7 +20,7 @@ import { Props as FormProps, FormState } from '../../forms/Form/types'
|
|||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { BuildFormStateArgs } from '../..'
|
import { BuildFormStateArgs } from '../..'
|
||||||
import { getFormState } from './getFormState'
|
import { getFormState } from '../../utilities/getFormState'
|
||||||
import { FieldPathProvider } from '../../forms/FieldPathProvider'
|
import { FieldPathProvider } from '../../forms/FieldPathProvider'
|
||||||
|
|
||||||
const baseClass = 'collection-edit'
|
const baseClass = 'collection-edit'
|
||||||
|
|||||||
Reference in New Issue
Block a user