feat: initial lexical support (#5206)
* chore: explores pattern for rscs in lexical * WORKING!!!!!! * fix(richtext-slate): field map path * Working Link Drawer * fix issues after merge * AlignFeature * Fix AlignFeature --------- Co-authored-by: James <james@trbl.design>
This commit is contained in:
20
.vscode/settings.json
vendored
20
.vscode/settings.json
vendored
@@ -36,26 +36,6 @@
|
|||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
// Load .git-blame-ignore-revs file
|
// Load .git-blame-ignore-revs file
|
||||||
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
|
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
|
||||||
"workbench.colorCustomizations": {
|
|
||||||
"activityBar.activeBackground": "#7fafca",
|
|
||||||
"activityBar.background": "#7fafca",
|
|
||||||
"activityBar.foreground": "#15202b",
|
|
||||||
"activityBar.inactiveForeground": "#15202b99",
|
|
||||||
"activityBarBadge.background": "#b44b8e",
|
|
||||||
"activityBarBadge.foreground": "#e7e7e7",
|
|
||||||
"commandCenter.border": "#15202b99",
|
|
||||||
"sash.hoverBorder": "#7fafca",
|
|
||||||
"statusBar.background": "#5b98bb",
|
|
||||||
"statusBar.foreground": "#15202b",
|
|
||||||
"statusBarItem.hoverBackground": "#437ea0",
|
|
||||||
"statusBarItem.remoteBackground": "#5b98bb",
|
|
||||||
"statusBarItem.remoteForeground": "#15202b",
|
|
||||||
"titleBar.activeBackground": "#5b98bb",
|
|
||||||
"titleBar.activeForeground": "#15202b",
|
|
||||||
"titleBar.inactiveBackground": "#5b98bb99",
|
|
||||||
"titleBar.inactiveForeground": "#15202b99"
|
|
||||||
},
|
|
||||||
"peacock.color": "#5b98bb",
|
|
||||||
"[javascript][typescript][typescriptreact]": {
|
"[javascript][typescript][typescriptreact]": {
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit"
|
"source.fixAll.eslint": "explicit"
|
||||||
|
|||||||
@@ -46,12 +46,13 @@
|
|||||||
"@types/json-schema": "7.0.15",
|
"@types/json-schema": "7.0.15",
|
||||||
"@types/node": "20.6.2",
|
"@types/node": "20.6.2",
|
||||||
"@types/react": "18.2.15",
|
"@types/react": "18.2.15",
|
||||||
|
"@types/react-dom": "18.2.7",
|
||||||
"payload": "workspace:*"
|
"payload": "workspace:*"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"payload": "^2.4.0",
|
|
||||||
"@payloadcms/translations": "workspace:^",
|
"@payloadcms/translations": "workspace:^",
|
||||||
"@payloadcms/ui": "workspace:^"
|
"@payloadcms/ui": "workspace:^",
|
||||||
|
"payload": "^2.4.0"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import { useTableCell } from '@payloadcms/ui/elements'
|
|||||||
import { $getRoot } from 'lexical'
|
import { $getRoot } from 'lexical'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
import type { SanitizedEditorConfig } from '../field/lexical/config/types'
|
import type { SanitizedClientEditorConfig } from '../field/lexical/config/types'
|
||||||
|
|
||||||
import { useFieldPath } from '../../../ui/src/forms/FieldPathProvider'
|
import { useFieldPath } from '../../../ui/src/forms/FieldPathProvider'
|
||||||
import { useClientFunctions } from '../../../ui/src/providers/ClientFunction'
|
import { useClientFunctions } from '../../../ui/src/providers/ClientFunction'
|
||||||
import { defaultEditorLexicalConfig } from '../field/lexical/config/defaultClient'
|
import { defaultEditorLexicalConfig } from '../field/lexical/config/client/default'
|
||||||
import { sanitizeEditorConfig } from '../field/lexical/config/sanitize'
|
import { sanitizeClientEditorConfig } from '../field/lexical/config/client/sanitize'
|
||||||
import { getEnabledNodes } from '../field/lexical/nodes'
|
import { getEnabledNodes } from '../field/lexical/nodes'
|
||||||
|
|
||||||
export const RichTextCell: React.FC<{
|
export const RichTextCell: React.FC<{
|
||||||
@@ -30,12 +30,10 @@ export const RichTextCell: React.FC<{
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalSanitizedEditorConfig: SanitizedEditorConfig = sanitizeEditorConfig({
|
const finalSanitizedEditorConfig: SanitizedClientEditorConfig = sanitizeClientEditorConfig(
|
||||||
features: [],
|
lexicalEditorConfig ? lexicalEditorConfig : defaultEditorLexicalConfig,
|
||||||
lexical: lexicalEditorConfig
|
null,
|
||||||
? () => Promise.resolve(lexicalEditorConfig)
|
)
|
||||||
: () => Promise.resolve(defaultEditorLexicalConfig),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Transform data through load hooks
|
// Transform data through load hooks
|
||||||
if (finalSanitizedEditorConfig?.features?.hooks?.load?.length) {
|
if (finalSanitizedEditorConfig?.features?.hooks?.load?.length) {
|
||||||
@@ -61,12 +59,11 @@ export const RichTextCell: React.FC<{
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
void finalSanitizedEditorConfig.lexical().then((lexicalConfig: LexicalEditorConfig) => {
|
|
||||||
// initialize headless editor
|
// initialize headless editor
|
||||||
const headlessEditor = createHeadlessEditor({
|
const headlessEditor = createHeadlessEditor({
|
||||||
namespace: lexicalConfig.namespace,
|
namespace: finalSanitizedEditorConfig.lexical.namespace,
|
||||||
nodes: getEnabledNodes({ editorConfig: finalSanitizedEditorConfig }),
|
nodes: getEnabledNodes({ editorConfig: finalSanitizedEditorConfig }),
|
||||||
theme: lexicalConfig.theme,
|
theme: finalSanitizedEditorConfig.lexical.theme,
|
||||||
})
|
})
|
||||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse))
|
headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse))
|
||||||
|
|
||||||
@@ -77,7 +74,6 @@ export const RichTextCell: React.FC<{
|
|||||||
|
|
||||||
// Limiting the number of characters shown is done in a CSS rule
|
// Limiting the number of characters shown is done in a CSS rule
|
||||||
setPreview(textContent)
|
setPreview(textContent)
|
||||||
})
|
|
||||||
}, [cellData, lexicalEditorConfig])
|
}, [cellData, lexicalEditorConfig])
|
||||||
|
|
||||||
return <span>{preview}</span>
|
return <span>{preview}</span>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export { RichTextCell } from '../cell'
|
export { RichTextCell } from '../cell'
|
||||||
export { RichTextField } from '../field'
|
export { RichTextField } from '../field'
|
||||||
|
|
||||||
export { defaultEditorLexicalConfig } from '../field/lexical/config/defaultClient'
|
export { defaultEditorLexicalConfig } from '../field/lexical/config/client/defaultClient'
|
||||||
export { ToolbarButton } from '../field/lexical/plugins/FloatingSelectToolbar/ToolbarButton'
|
export { ToolbarButton } from '../field/lexical/plugins/FloatingSelectToolbar/ToolbarButton'
|
||||||
export { ToolbarDropdown } from '../field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/index'
|
export { ToolbarDropdown } from '../field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/index'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { type FormFieldBase, useField } from '@payloadcms/ui'
|
|||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { ErrorBoundary } from 'react-error-boundary'
|
import { ErrorBoundary } from 'react-error-boundary'
|
||||||
|
|
||||||
import type { SanitizedEditorConfig } from './lexical/config/types'
|
import type { SanitizedClientEditorConfig } from './lexical/config/types'
|
||||||
|
|
||||||
import { richTextValidateHOC } from '../validate'
|
import { richTextValidateHOC } from '../validate'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
@@ -15,7 +15,7 @@ const baseClass = 'rich-text-lexical'
|
|||||||
|
|
||||||
const RichText: React.FC<
|
const RichText: React.FC<
|
||||||
FormFieldBase & {
|
FormFieldBase & {
|
||||||
editorConfig: SanitizedEditorConfig // With rendered features n stuff
|
editorConfig: SanitizedClientEditorConfig // With rendered features n stuff
|
||||||
name: string
|
name: string
|
||||||
richTextComponentMap: Map<string, React.ReactNode>
|
richTextComponentMap: Map<string, React.ReactNode>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '@payloadcms/ui/scss';
|
@import '../../../../../../ui/src/scss/styles.scss';
|
||||||
|
|
||||||
.ContentEditable__root > div:has(.lexical-block) {
|
.ContentEditable__root > div:has(.lexical-block) {
|
||||||
// Fixes a bug where, if the block field has a Select field, the Select field's dropdown menu would be hidden behind the lexical editor.
|
// Fixes a bug where, if the block field has a Select field, the Select field's dropdown menu would be hidden behind the lexical editor.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { sanitizeFields } from 'payload/config'
|
|||||||
|
|
||||||
import type { BlocksFeatureProps } from '..'
|
import type { BlocksFeatureProps } from '..'
|
||||||
|
|
||||||
import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider'
|
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider'
|
||||||
import { transformInputFormSchema } from '../utils/transformInputFormSchema'
|
import { transformInputFormSchema } from '../utils/transformInputFormSchema'
|
||||||
import { BlockContent } from './BlockContent'
|
import { BlockContent } from './BlockContent'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import React, { useCallback, useEffect, useState } from 'react'
|
|||||||
|
|
||||||
import type { BlocksFeatureProps } from '..'
|
import type { BlocksFeatureProps } from '..'
|
||||||
|
|
||||||
import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider'
|
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider'
|
||||||
import { $createBlockNode } from '../nodes/BlocksNode'
|
import { $createBlockNode } from '../nodes/BlocksNode'
|
||||||
import { INSERT_BLOCK_COMMAND } from '../plugin/commands'
|
import { INSERT_BLOCK_COMMAND } from '../plugin/commands'
|
||||||
const baseClass = 'lexical-blocks-drawer'
|
const baseClass = 'lexical-blocks-drawer'
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import { Drawer, Form, FormSubmit, RenderFields, fieldTypes, useTranslation } from '@payloadcms/ui'
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
import './index.scss'
|
|
||||||
import { type Props } from './types'
|
|
||||||
|
|
||||||
const baseClass = 'lexical-link-edit-drawer'
|
|
||||||
|
|
||||||
export const LinkDrawer: React.FC<Props> = ({
|
|
||||||
drawerSlug,
|
|
||||||
fieldSchema,
|
|
||||||
handleModalSubmit,
|
|
||||||
initialState,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer className={baseClass} slug={drawerSlug} title={t('fields:editLink') ?? ''}>
|
|
||||||
<Form fields={fieldSchema} initialState={initialState} onSubmit={handleModalSubmit}>
|
|
||||||
[RenderFields]
|
|
||||||
{/* <RenderFields
|
|
||||||
fieldSchema={fieldSchema}
|
|
||||||
fieldTypes={fieldTypes}
|
|
||||||
forceRender
|
|
||||||
readOnly={false}
|
|
||||||
/> */}
|
|
||||||
<FormSubmit>{t('general:submit')}</FormSubmit>
|
|
||||||
</Form>
|
|
||||||
</Drawer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
import type { I18n } from '@payloadcms/translations'
|
|
||||||
import type { SanitizedConfig } from 'payload/config'
|
|
||||||
import type { Field } from 'payload/types'
|
|
||||||
|
|
||||||
import { $findMatchingParent } from '@lexical/utils'
|
|
||||||
import { $getSelection, $isRangeSelection } from 'lexical'
|
|
||||||
|
|
||||||
import type { HTMLConverter } from '../converters/html/converter/types'
|
|
||||||
import type { FeatureProvider } from '../types'
|
|
||||||
import type { SerializedAutoLinkNode } from './nodes/AutoLinkNode'
|
|
||||||
import type { LinkFields, SerializedLinkNode } from './nodes/LinkNode'
|
|
||||||
|
|
||||||
import { getSelectedNode } from '../../lexical/utils/getSelectedNode'
|
|
||||||
import { FeaturesSectionWithEntries } from '../common/floatingSelectToolbarFeaturesButtonsSection'
|
|
||||||
import { convertLexicalNodesToHTML } from '../converters/html/converter'
|
|
||||||
import { AutoLinkNode } from './nodes/AutoLinkNode'
|
|
||||||
import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from './nodes/LinkNode'
|
|
||||||
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './plugins/floatingLinkEditor/LinkEditor/commands'
|
|
||||||
import { linkPopulationPromiseHOC } from './populationPromise'
|
|
||||||
|
|
||||||
type ExclusiveLinkCollectionsProps =
|
|
||||||
| {
|
|
||||||
/**
|
|
||||||
* The collections that should be disabled for internal linking. Overrides the `enableRichTextLink` property in the collection config.
|
|
||||||
* When this property is set, `enabledCollections` will not be available.
|
|
||||||
**/
|
|
||||||
disabledCollections?: string[]
|
|
||||||
|
|
||||||
// Ensures that enabledCollections is not available when disabledCollections is set
|
|
||||||
enabledCollections?: never
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
// Ensures that disabledCollections is not available when enabledCollections is set
|
|
||||||
disabledCollections?: never
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The collections that should be enabled for internal linking. Overrides the `enableRichTextLink` property in the collection config
|
|
||||||
* When this property is set, `disabledCollections` will not be available.
|
|
||||||
**/
|
|
||||||
enabledCollections?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LinkFeatureProps = ExclusiveLinkCollectionsProps & {
|
|
||||||
/**
|
|
||||||
* A function or array defining additional fields for the link feature. These will be
|
|
||||||
* displayed in the link editor drawer.
|
|
||||||
*/
|
|
||||||
fields?:
|
|
||||||
| ((args: { config: SanitizedConfig; defaultFields: Field[]; i18n: I18n }) => Field[])
|
|
||||||
| Field[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
|
|
||||||
return {
|
|
||||||
feature: () => {
|
|
||||||
return {
|
|
||||||
floatingSelectToolbar: {
|
|
||||||
sections: [
|
|
||||||
FeaturesSectionWithEntries([
|
|
||||||
{
|
|
||||||
ChildComponent: () =>
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
import('../../lexical/ui/icons/Link').then((module) => module.LinkIcon),
|
|
||||||
isActive: ({ selection }) => {
|
|
||||||
if ($isRangeSelection(selection)) {
|
|
||||||
const selectedNode = getSelectedNode(selection)
|
|
||||||
const linkParent = $findMatchingParent(selectedNode, $isLinkNode)
|
|
||||||
return linkParent != null
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
key: 'link',
|
|
||||||
label: `Link`,
|
|
||||||
onClick: ({ editor, isActive }) => {
|
|
||||||
if (!isActive) {
|
|
||||||
let selectedText = null
|
|
||||||
editor.getEditorState().read(() => {
|
|
||||||
selectedText = $getSelection().getTextContent()
|
|
||||||
})
|
|
||||||
const linkFields: LinkFields = {
|
|
||||||
doc: null,
|
|
||||||
linkType: 'custom',
|
|
||||||
newTab: false,
|
|
||||||
url: 'https://',
|
|
||||||
}
|
|
||||||
editor.dispatchCommand(TOGGLE_LINK_WITH_MODAL_COMMAND, {
|
|
||||||
fields: linkFields,
|
|
||||||
text: selectedText,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// remove link
|
|
||||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
order: 1,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
type: LinkNode.getType(),
|
|
||||||
converters: {
|
|
||||||
html: {
|
|
||||||
converter: async ({ converters, node, parent }) => {
|
|
||||||
const childrenText = await convertLexicalNodesToHTML({
|
|
||||||
converters,
|
|
||||||
lexicalNodes: node.children,
|
|
||||||
parent: {
|
|
||||||
...node,
|
|
||||||
parent,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : ''
|
|
||||||
|
|
||||||
const href: string =
|
|
||||||
node.fields.linkType === 'custom'
|
|
||||||
? node.fields.url
|
|
||||||
: (node.fields.doc?.value as string)
|
|
||||||
|
|
||||||
return `<a href="${href}"${rel}>${childrenText}</a>`
|
|
||||||
},
|
|
||||||
nodeTypes: [LinkNode.getType()],
|
|
||||||
} as HTMLConverter<SerializedLinkNode>,
|
|
||||||
},
|
|
||||||
node: LinkNode,
|
|
||||||
populationPromises: [linkPopulationPromiseHOC(props)],
|
|
||||||
// TODO: Add validation similar to upload for internal links and fields
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: AutoLinkNode.getType(),
|
|
||||||
converters: {
|
|
||||||
html: {
|
|
||||||
converter: async ({ converters, node, parent }) => {
|
|
||||||
const childrenText = await convertLexicalNodesToHTML({
|
|
||||||
converters,
|
|
||||||
lexicalNodes: node.children,
|
|
||||||
parent: {
|
|
||||||
...node,
|
|
||||||
parent,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : ''
|
|
||||||
|
|
||||||
let href: string = node.fields.url
|
|
||||||
if (node.fields.linkType === 'internal') {
|
|
||||||
href =
|
|
||||||
typeof node.fields.doc?.value === 'string'
|
|
||||||
? node.fields.doc?.value
|
|
||||||
: node.fields.doc?.value?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<a href="${href}"${rel}>${childrenText}</a>`
|
|
||||||
},
|
|
||||||
nodeTypes: [AutoLinkNode.getType()],
|
|
||||||
} as HTMLConverter<SerializedAutoLinkNode>,
|
|
||||||
},
|
|
||||||
node: AutoLinkNode,
|
|
||||||
populationPromises: [linkPopulationPromiseHOC(props)],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
{
|
|
||||||
Component: () =>
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
import('./plugins/link').then((module) => module.LinkPlugin),
|
|
||||||
position: 'normal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Component: () =>
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
import('./plugins/autoLink').then((module) => module.AutoLinkPlugin),
|
|
||||||
position: 'normal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Component: () =>
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
import('./plugins/clickableLink').then((module) => module.ClickableLinkPlugin),
|
|
||||||
position: 'normal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Component: () =>
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
import('./plugins/floatingLinkEditor').then((module) => {
|
|
||||||
const floatingLinkEditorPlugin = module.FloatingLinkEditorPlugin
|
|
||||||
return import('@payloadcms/ui').then((module) =>
|
|
||||||
module.withMergedProps({
|
|
||||||
Component: floatingLinkEditorPlugin,
|
|
||||||
toMergeIntoProps: props,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
position: 'floatingAnchorElem',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
props,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
key: 'link',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import * as React from 'react'
|
|
||||||
import { createPortal } from 'react-dom'
|
|
||||||
|
|
||||||
import type { LinkFeatureProps } from '../..'
|
|
||||||
|
|
||||||
import { LinkEditor } from './LinkEditor'
|
|
||||||
import './index.scss'
|
|
||||||
|
|
||||||
export const FloatingLinkEditorPlugin: React.FC<
|
|
||||||
{
|
|
||||||
anchorElem: HTMLElement
|
|
||||||
} & LinkFeatureProps
|
|
||||||
> = (props) => {
|
|
||||||
const { anchorElem = document.body } = props
|
|
||||||
|
|
||||||
return createPortal(<LinkEditor {...props} anchorElem={anchorElem} />, anchorElem)
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import React, { useCallback, useReducer, useState } from 'react'
|
|||||||
|
|
||||||
import type { RelationshipData } from '../RelationshipNode'
|
import type { RelationshipData } from '../RelationshipNode'
|
||||||
|
|
||||||
import { useEditorConfigContext } from '../../../../lexical/config/EditorConfigProvider'
|
import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider'
|
||||||
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from '../../drawer/commands'
|
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from '../../drawer/commands'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '@payloadcms/ui/scss';
|
@import '../../../../../../../ui/src/scss/styles.scss';
|
||||||
|
|
||||||
.lexical-relationship {
|
.lexical-relationship {
|
||||||
@extend %body;
|
@extend %body;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import type { ElementProps } from '..'
|
|||||||
import type { UploadFeatureProps } from '../..'
|
import type { UploadFeatureProps } from '../..'
|
||||||
import type { UploadData, UploadNode } from '../../nodes/UploadNode'
|
import type { UploadData, UploadNode } from '../../nodes/UploadNode'
|
||||||
|
|
||||||
import { useEditorConfigContext } from '../../../../lexical/config/EditorConfigProvider'
|
import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This handles the extra fields, e.g. captions or alt text, which are
|
* This handles the extra fields, e.g. captions or alt text, which are
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '@payloadcms/ui/scss';
|
@import '../../../../../../ui/src/scss/styles.scss';
|
||||||
|
|
||||||
.lexical-upload {
|
.lexical-upload {
|
||||||
@extend %body;
|
@extend %body;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import React, { useCallback, useReducer, useState } from 'react'
|
|||||||
import type { UploadFeatureProps } from '..'
|
import type { UploadFeatureProps } from '..'
|
||||||
import type { UploadData } from '../nodes/UploadNode'
|
import type { UploadData } from '../nodes/UploadNode'
|
||||||
|
|
||||||
import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider'
|
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider'
|
||||||
import { EnabledRelationshipsCondition } from '../../Relationship/utils/EnabledRelationshipsCondition'
|
import { EnabledRelationshipsCondition } from '../../Relationship/utils/EnabledRelationshipsCondition'
|
||||||
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from '../drawer/commands'
|
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from '../drawer/commands'
|
||||||
import { ExtraFieldsUploadDrawer } from './ExtraFieldsDrawer'
|
import { ExtraFieldsUploadDrawer } from './ExtraFieldsDrawer'
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { FORMAT_ELEMENT_COMMAND } from 'lexical'
|
||||||
|
|
||||||
|
import type { FeatureProviderProviderClient } from '../types'
|
||||||
|
|
||||||
|
import { AlignCenterIcon } from '../../lexical/ui/icons/AlignCenter'
|
||||||
|
import { AlignJustifyIcon } from '../../lexical/ui/icons/AlignJustify'
|
||||||
|
import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft'
|
||||||
|
import { AlignRightIcon } from '../../lexical/ui/icons/AlignRight'
|
||||||
|
import { createClientComponent } from '../createClientComponent'
|
||||||
|
import { AlignDropdownSectionWithEntries } from './floatingSelectToolbarAlignDropdownSection'
|
||||||
|
|
||||||
|
const AlignFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
|
||||||
|
return {
|
||||||
|
clientFeatureProps: props,
|
||||||
|
feature: () => ({
|
||||||
|
clientFeatureProps: props,
|
||||||
|
floatingSelectToolbar: {
|
||||||
|
sections: [
|
||||||
|
AlignDropdownSectionWithEntries([
|
||||||
|
{
|
||||||
|
ChildComponent: AlignLeftIcon,
|
||||||
|
isActive: () => false,
|
||||||
|
key: 'align-left',
|
||||||
|
label: `Align Left`,
|
||||||
|
onClick: ({ editor }) => {
|
||||||
|
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left')
|
||||||
|
},
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
AlignDropdownSectionWithEntries([
|
||||||
|
{
|
||||||
|
ChildComponent: AlignCenterIcon,
|
||||||
|
isActive: () => false,
|
||||||
|
key: 'align-center',
|
||||||
|
label: `Align Center`,
|
||||||
|
onClick: ({ editor }) => {
|
||||||
|
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center')
|
||||||
|
},
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
AlignDropdownSectionWithEntries([
|
||||||
|
{
|
||||||
|
ChildComponent: AlignRightIcon,
|
||||||
|
isActive: () => false,
|
||||||
|
key: 'align-right',
|
||||||
|
label: `Align Right`,
|
||||||
|
onClick: ({ editor }) => {
|
||||||
|
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right')
|
||||||
|
},
|
||||||
|
order: 3,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
AlignDropdownSectionWithEntries([
|
||||||
|
{
|
||||||
|
ChildComponent: AlignJustifyIcon,
|
||||||
|
isActive: () => false,
|
||||||
|
key: 'align-justify',
|
||||||
|
label: `Align Justify`,
|
||||||
|
onClick: ({ editor }) => {
|
||||||
|
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify')
|
||||||
|
},
|
||||||
|
order: 4,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AlignFeatureClientComponent = createClientComponent(AlignFeatureClient)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { FeatureProviderProviderServer } from '../types'
|
||||||
|
|
||||||
|
import { AlignFeatureClientComponent } from './feature.client'
|
||||||
|
|
||||||
|
export const AlignFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
|
||||||
|
return {
|
||||||
|
feature: () => {
|
||||||
|
return {
|
||||||
|
ClientComponent: AlignFeatureClientComponent,
|
||||||
|
clientFeatureProps: null,
|
||||||
|
serverFeatureProps: props,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key: 'align',
|
||||||
|
serverFeatureProps: props,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,14 +3,14 @@ import type {
|
|||||||
FloatingToolbarSectionEntry,
|
FloatingToolbarSectionEntry,
|
||||||
} from '../../lexical/plugins/FloatingSelectToolbar/types'
|
} from '../../lexical/plugins/FloatingSelectToolbar/types'
|
||||||
|
|
||||||
|
import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft'
|
||||||
|
|
||||||
export const AlignDropdownSectionWithEntries = (
|
export const AlignDropdownSectionWithEntries = (
|
||||||
entries: FloatingToolbarSectionEntry[],
|
entries: FloatingToolbarSectionEntry[],
|
||||||
): FloatingToolbarSection => {
|
): FloatingToolbarSection => {
|
||||||
return {
|
return {
|
||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
ChildComponent: () =>
|
ChildComponent: AlignLeftIcon,
|
||||||
// @ts-expect-error-next-line
|
|
||||||
import('../../lexical/ui/icons/AlignLeft').then((module) => module.AlignLeftIcon),
|
|
||||||
entries,
|
entries,
|
||||||
key: 'dropdown-align',
|
key: 'dropdown-align',
|
||||||
order: 2,
|
order: 2,
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import { FORMAT_ELEMENT_COMMAND } from 'lexical'
|
|
||||||
|
|
||||||
import type { FeatureProvider } from '../types'
|
|
||||||
|
|
||||||
import { AlignDropdownSectionWithEntries } from './floatingSelectToolbarAlignDropdownSection'
|
|
||||||
|
|
||||||
export const AlignFeature = (): FeatureProvider => {
|
|
||||||
return {
|
|
||||||
feature: () => {
|
|
||||||
return {
|
|
||||||
floatingSelectToolbar: {
|
|
||||||
sections: [
|
|
||||||
AlignDropdownSectionWithEntries([
|
|
||||||
{
|
|
||||||
ChildComponent: () =>
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
import('../../lexical/ui/icons/AlignLeft').then((module) => module.AlignLeftIcon),
|
|
||||||
isActive: () => false,
|
|
||||||
key: 'align-left',
|
|
||||||
label: `Align Left`,
|
|
||||||
onClick: ({ editor }) => {
|
|
||||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left')
|
|
||||||
},
|
|
||||||
order: 1,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
AlignDropdownSectionWithEntries([
|
|
||||||
{
|
|
||||||
ChildComponent: () =>
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
import('../../lexical/ui/icons/AlignCenter').then(
|
|
||||||
(module) => module.AlignCenterIcon,
|
|
||||||
),
|
|
||||||
isActive: () => false,
|
|
||||||
key: 'align-center',
|
|
||||||
label: `Align Center`,
|
|
||||||
onClick: ({ editor }) => {
|
|
||||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center')
|
|
||||||
},
|
|
||||||
order: 2,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
AlignDropdownSectionWithEntries([
|
|
||||||
{
|
|
||||||
ChildComponent: () =>
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
import('../../lexical/ui/icons/AlignRight').then(
|
|
||||||
(module) => module.AlignRightIcon,
|
|
||||||
),
|
|
||||||
isActive: () => false,
|
|
||||||
key: 'align-right',
|
|
||||||
label: `Align Right`,
|
|
||||||
onClick: ({ editor }) => {
|
|
||||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right')
|
|
||||||
},
|
|
||||||
order: 3,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
AlignDropdownSectionWithEntries([
|
|
||||||
{
|
|
||||||
ChildComponent: () =>
|
|
||||||
// @ts-expect-error
|
|
||||||
import('../../lexical/ui/icons/AlignJustify').then(
|
|
||||||
(module) => module.AlignJustifyIcon,
|
|
||||||
),
|
|
||||||
isActive: () => false,
|
|
||||||
key: 'align-justify',
|
|
||||||
label: `Align Justify`,
|
|
||||||
onClick: ({ editor }) => {
|
|
||||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify')
|
|
||||||
},
|
|
||||||
order: 4,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
props: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
key: 'align',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { FeatureProviderProviderClient, ServerFeature } from './types'
|
||||||
|
|
||||||
|
import { useLexicalFeature } from '../../useLexicalFeature'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to create a client component for the client feature
|
||||||
|
*/
|
||||||
|
export const createClientComponent = <ClientFeatureProps,>(
|
||||||
|
clientFeature: FeatureProviderProviderClient<ClientFeatureProps>,
|
||||||
|
): ServerFeature<unknown, ClientFeatureProps>['ClientComponent'] => {
|
||||||
|
return (props) => {
|
||||||
|
useLexicalFeature(props.featureKey, clientFeature(props))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '@payloadcms/ui/scss';
|
@import '../../../../../../ui/src/scss/styles.scss';
|
||||||
|
|
||||||
.lexical-link-edit-drawer {
|
.lexical-link-edit-drawer {
|
||||||
&__template {
|
&__template {
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
FieldPathProvider,
|
||||||
|
Form,
|
||||||
|
type FormState,
|
||||||
|
FormSubmit,
|
||||||
|
RenderFields,
|
||||||
|
getFormState,
|
||||||
|
useConfig,
|
||||||
|
useDocumentInfo,
|
||||||
|
useTranslation,
|
||||||
|
} from '@payloadcms/ui'
|
||||||
|
import { useFieldPath } from '@payloadcms/ui'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider'
|
||||||
|
import './index.scss'
|
||||||
|
import { type Props } from './types'
|
||||||
|
|
||||||
|
const baseClass = 'lexical-link-edit-drawer'
|
||||||
|
|
||||||
|
export const LinkDrawer: React.FC<Props> = ({ drawerSlug, handleModalSubmit, stateData }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { id, getDocPreferences } = useDocumentInfo()
|
||||||
|
const { schemaPath } = useFieldPath()
|
||||||
|
const config = useConfig()
|
||||||
|
const [initialState, setInitialState] = useState<FormState>({})
|
||||||
|
const {
|
||||||
|
field: { richTextComponentMap },
|
||||||
|
} = useEditorConfigContext()
|
||||||
|
|
||||||
|
const componentMapRenderedFieldsPath = `feature.link.fields.fields`
|
||||||
|
const schemaFieldsPath = `${schemaPath}.feature.link.fields`
|
||||||
|
|
||||||
|
const fieldMap = richTextComponentMap.get(componentMapRenderedFieldsPath) // Field Schema
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const awaitInitialState = async () => {
|
||||||
|
const docPreferences = await getDocPreferences()
|
||||||
|
|
||||||
|
const state = await getFormState({
|
||||||
|
apiRoute: config.routes.api,
|
||||||
|
body: {
|
||||||
|
id,
|
||||||
|
data: stateData,
|
||||||
|
docPreferences,
|
||||||
|
operation: 'update',
|
||||||
|
schemaPath: schemaFieldsPath,
|
||||||
|
},
|
||||||
|
serverURL: config.serverURL,
|
||||||
|
}) // Form State
|
||||||
|
|
||||||
|
setInitialState(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateData) {
|
||||||
|
void awaitInitialState()
|
||||||
|
}
|
||||||
|
}, [config.routes.api, config.serverURL, schemaFieldsPath, getDocPreferences, id, stateData])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer className={baseClass} slug={drawerSlug} title={t('fields:editLink') ?? ''}>
|
||||||
|
<FieldPathProvider path="" schemaPath="">
|
||||||
|
<Form fields={fieldMap} initialState={initialState} onSubmit={handleModalSubmit}>
|
||||||
|
<RenderFields fieldMap={fieldMap} forceRender readOnly={false} />
|
||||||
|
|
||||||
|
<FormSubmit>{t('general:submit')}</FormSubmit>
|
||||||
|
</Form>
|
||||||
|
</FieldPathProvider>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { FormState } from '@payloadcms/ui'
|
import type { FormState } from '@payloadcms/ui'
|
||||||
|
|
||||||
import { type Field } from 'payload/types'
|
import type { LinkPayload } from '../plugins/floatingLinkEditor/types'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
drawerSlug: string
|
drawerSlug: string
|
||||||
fieldSchema: Field[]
|
|
||||||
handleModalSubmit: (fields: FormState, data: Record<string, unknown>) => void
|
handleModalSubmit: (fields: FormState, data: Record<string, unknown>) => void
|
||||||
initialState?: FormState
|
stateData?: LinkPayload
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { $findMatchingParent } from '@lexical/utils'
|
||||||
|
import { $getSelection, $isRangeSelection } from 'lexical'
|
||||||
|
|
||||||
|
import type { FeatureProviderProviderClient } from '../types'
|
||||||
|
import type { ExclusiveLinkCollectionsProps } from './feature.server'
|
||||||
|
import type { LinkFields } from './nodes/types'
|
||||||
|
|
||||||
|
import { LinkIcon } from '../../lexical/ui/icons/Link'
|
||||||
|
import { getSelectedNode } from '../../lexical/utils/getSelectedNode'
|
||||||
|
import { FeaturesSectionWithEntries } from '../common/floatingSelectToolbarFeaturesButtonsSection'
|
||||||
|
import { createClientComponent } from '../createClientComponent'
|
||||||
|
import { AutoLinkNode } from './nodes/AutoLinkNode'
|
||||||
|
import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from './nodes/LinkNode'
|
||||||
|
import { AutoLinkPlugin } from './plugins/autoLink'
|
||||||
|
import { ClickableLinkPlugin } from './plugins/clickableLink'
|
||||||
|
import { FloatingLinkEditorPlugin } from './plugins/floatingLinkEditor'
|
||||||
|
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './plugins/floatingLinkEditor/LinkEditor/commands'
|
||||||
|
import { LinkPlugin } from './plugins/link'
|
||||||
|
|
||||||
|
export type ClientProps = ExclusiveLinkCollectionsProps
|
||||||
|
|
||||||
|
const LinkFeatureClient: FeatureProviderProviderClient<ClientProps> = (props) => {
|
||||||
|
return {
|
||||||
|
clientFeatureProps: props,
|
||||||
|
feature: () => ({
|
||||||
|
clientFeatureProps: props,
|
||||||
|
floatingSelectToolbar: {
|
||||||
|
sections: [
|
||||||
|
FeaturesSectionWithEntries([
|
||||||
|
{
|
||||||
|
ChildComponent: LinkIcon,
|
||||||
|
isActive: ({ selection }) => {
|
||||||
|
if ($isRangeSelection(selection)) {
|
||||||
|
const selectedNode = getSelectedNode(selection)
|
||||||
|
const linkParent = $findMatchingParent(selectedNode, $isLinkNode)
|
||||||
|
return linkParent != null
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
key: 'link',
|
||||||
|
label: `Link`,
|
||||||
|
onClick: ({ editor, isActive }) => {
|
||||||
|
if (!isActive) {
|
||||||
|
let selectedText = null
|
||||||
|
editor.getEditorState().read(() => {
|
||||||
|
selectedText = $getSelection().getTextContent()
|
||||||
|
})
|
||||||
|
const linkFields: LinkFields = {
|
||||||
|
doc: null,
|
||||||
|
linkType: 'custom',
|
||||||
|
newTab: false,
|
||||||
|
url: 'https://',
|
||||||
|
}
|
||||||
|
editor.dispatchCommand(TOGGLE_LINK_WITH_MODAL_COMMAND, {
|
||||||
|
fields: linkFields,
|
||||||
|
text: selectedText,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// remove link
|
||||||
|
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
nodes: [LinkNode, AutoLinkNode],
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
Component: LinkPlugin,
|
||||||
|
position: 'normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Component: AutoLinkPlugin,
|
||||||
|
position: 'normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Component: ClickableLinkPlugin,
|
||||||
|
position: 'normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Component: FloatingLinkEditorPlugin,
|
||||||
|
position: 'floatingAnchorElem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkFeatureClientComponent = createClientComponent(LinkFeatureClient)
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import type { I18n } from '@payloadcms/translations'
|
||||||
|
import type { SanitizedConfig } from 'payload/config'
|
||||||
|
import type { Field } from 'payload/types'
|
||||||
|
import type React from 'react'
|
||||||
|
|
||||||
|
import { initI18n } from '@payloadcms/translations'
|
||||||
|
import { translations } from '@payloadcms/translations/client'
|
||||||
|
|
||||||
|
import type { HTMLConverter } from '../converters/html/converter/types'
|
||||||
|
import type { FeatureProviderProviderServer } from '../types'
|
||||||
|
import type { ClientProps } from './feature.client'
|
||||||
|
import type { SerializedAutoLinkNode, SerializedLinkNode } from './nodes/types'
|
||||||
|
|
||||||
|
import { convertLexicalNodesToHTML } from '../converters/html/converter'
|
||||||
|
import { LinkFeatureClientComponent } from './feature.client'
|
||||||
|
import { AutoLinkNode } from './nodes/AutoLinkNode'
|
||||||
|
import { LinkNode } from './nodes/LinkNode'
|
||||||
|
import { transformExtraFields } from './plugins/floatingLinkEditor/utilities'
|
||||||
|
import { linkPopulationPromiseHOC } from './populationPromise'
|
||||||
|
|
||||||
|
export type ExclusiveLinkCollectionsProps =
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* The collections that should be disabled for internal linking. Overrides the `enableRichTextLink` property in the collection config.
|
||||||
|
* When this property is set, `enabledCollections` will not be available.
|
||||||
|
**/
|
||||||
|
disabledCollections?: string[]
|
||||||
|
|
||||||
|
// Ensures that enabledCollections is not available when disabledCollections is set
|
||||||
|
enabledCollections?: never
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// Ensures that disabledCollections is not available when enabledCollections is set
|
||||||
|
disabledCollections?: never
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The collections that should be enabled for internal linking. Overrides the `enableRichTextLink` property in the collection config
|
||||||
|
* When this property is set, `disabledCollections` will not be available.
|
||||||
|
**/
|
||||||
|
enabledCollections?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LinkFeatureServerProps = ExclusiveLinkCollectionsProps & {
|
||||||
|
/**
|
||||||
|
* A function or array defining additional fields for the link feature. These will be
|
||||||
|
* displayed in the link editor drawer.
|
||||||
|
*/
|
||||||
|
fields?:
|
||||||
|
| ((args: { config: SanitizedConfig; defaultFields: Field[]; i18n: I18n }) => Field[])
|
||||||
|
| Field[]
|
||||||
|
|
||||||
|
//someFunction: (something: number) => string
|
||||||
|
someFunction?: React.FC
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps, ClientProps> = (
|
||||||
|
props,
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
feature: () => {
|
||||||
|
return {
|
||||||
|
ClientComponent: LinkFeatureClientComponent,
|
||||||
|
clientFeatureProps: {
|
||||||
|
disabledCollections: props.disabledCollections,
|
||||||
|
enabledCollections: props.enabledCollections,
|
||||||
|
} as ExclusiveLinkCollectionsProps,
|
||||||
|
generateComponentMap: () => {
|
||||||
|
return {
|
||||||
|
someFunction: props.someFunction,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
generateSchemaMap: ({ config, props, schemaMap, schemaPath }) => {
|
||||||
|
const i18n = initI18n({ config: config.i18n, context: 'client', translations })
|
||||||
|
|
||||||
|
return {
|
||||||
|
fields: transformExtraFields(
|
||||||
|
props.fields,
|
||||||
|
config,
|
||||||
|
i18n,
|
||||||
|
props.enabledCollections,
|
||||||
|
props.disabledCollections,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
converters: {
|
||||||
|
html: {
|
||||||
|
converter: async ({ converters, node, parent }) => {
|
||||||
|
const childrenText = await convertLexicalNodesToHTML({
|
||||||
|
converters,
|
||||||
|
lexicalNodes: node.children,
|
||||||
|
parent: {
|
||||||
|
...node,
|
||||||
|
parent,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : ''
|
||||||
|
|
||||||
|
let href: string = node.fields.url
|
||||||
|
if (node.fields.linkType === 'internal') {
|
||||||
|
href =
|
||||||
|
typeof node.fields.doc?.value === 'string'
|
||||||
|
? node.fields.doc?.value
|
||||||
|
: node.fields.doc?.value?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<a href="${href}"${rel}>${childrenText}</a>`
|
||||||
|
},
|
||||||
|
nodeTypes: [AutoLinkNode.getType()],
|
||||||
|
} as HTMLConverter<SerializedAutoLinkNode>,
|
||||||
|
},
|
||||||
|
node: AutoLinkNode,
|
||||||
|
populationPromises: [linkPopulationPromiseHOC(props)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
converters: {
|
||||||
|
html: {
|
||||||
|
converter: async ({ converters, node, parent }) => {
|
||||||
|
const childrenText = await convertLexicalNodesToHTML({
|
||||||
|
converters,
|
||||||
|
lexicalNodes: node.children,
|
||||||
|
parent: {
|
||||||
|
...node,
|
||||||
|
parent,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : ''
|
||||||
|
|
||||||
|
const href: string =
|
||||||
|
node.fields.linkType === 'custom'
|
||||||
|
? node.fields.url
|
||||||
|
: (node.fields.doc?.value as string)
|
||||||
|
|
||||||
|
return `<a href="${href}"${rel}>${childrenText}</a>`
|
||||||
|
},
|
||||||
|
nodeTypes: [LinkNode.getType()],
|
||||||
|
} as HTMLConverter<SerializedLinkNode>,
|
||||||
|
},
|
||||||
|
node: LinkNode,
|
||||||
|
populationPromises: [linkPopulationPromiseHOC(props)],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
serverFeatureProps: props,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key: 'link',
|
||||||
|
serverFeatureProps: props,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,9 @@ import {
|
|||||||
type RangeSelection,
|
type RangeSelection,
|
||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
|
|
||||||
import { type LinkFields, LinkNode, type SerializedLinkNode } from './LinkNode'
|
import type { LinkFields, SerializedAutoLinkNode } from './types'
|
||||||
|
|
||||||
export type SerializedAutoLinkNode = SerializedLinkNode
|
import { LinkNode } from './LinkNode'
|
||||||
|
|
||||||
// Custom node type to override `canInsertTextAfter` that will
|
// Custom node type to override `canInsertTextAfter` that will
|
||||||
// allow typing within the link
|
// allow typing within the link
|
||||||
@@ -15,39 +15,11 @@ import {
|
|||||||
type LexicalNode,
|
type LexicalNode,
|
||||||
type NodeKey,
|
type NodeKey,
|
||||||
type RangeSelection,
|
type RangeSelection,
|
||||||
type SerializedElementNode,
|
|
||||||
type Spread,
|
|
||||||
createCommand,
|
createCommand,
|
||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
|
|
||||||
import type { LinkPayload } from '../plugins/floatingLinkEditor/types'
|
import type { LinkPayload } from '../plugins/floatingLinkEditor/types'
|
||||||
|
import type { LinkFields, SerializedLinkNode } from './types'
|
||||||
import { $isAutoLinkNode } from './AutoLinkNode'
|
|
||||||
|
|
||||||
export type LinkFields = {
|
|
||||||
// unknown, custom fields:
|
|
||||||
[key: string]: unknown
|
|
||||||
doc: {
|
|
||||||
relationTo: string
|
|
||||||
value:
|
|
||||||
| {
|
|
||||||
// Actual doc data, populated in afterRead hook
|
|
||||||
[key: string]: unknown
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
| string
|
|
||||||
} | null
|
|
||||||
linkType: 'custom' | 'internal'
|
|
||||||
newTab: boolean
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SerializedLinkNode = Spread<
|
|
||||||
{
|
|
||||||
fields: LinkFields
|
|
||||||
},
|
|
||||||
SerializedElementNode
|
|
||||||
>
|
|
||||||
|
|
||||||
const SUPPORTED_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'sms:', 'tel:'])
|
const SUPPORTED_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'sms:', 'tel:'])
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { SerializedElementNode, Spread } from 'lexical'
|
||||||
|
|
||||||
|
export type LinkFields = {
|
||||||
|
// unknown, custom fields:
|
||||||
|
[key: string]: unknown
|
||||||
|
doc: {
|
||||||
|
relationTo: string
|
||||||
|
value:
|
||||||
|
| {
|
||||||
|
// Actual doc data, populated in afterRead hook
|
||||||
|
[key: string]: unknown
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
| string
|
||||||
|
} | null
|
||||||
|
linkType: 'custom' | 'internal'
|
||||||
|
newTab: boolean
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SerializedLinkNode = Spread<
|
||||||
|
{
|
||||||
|
fields: LinkFields
|
||||||
|
},
|
||||||
|
SerializedElementNode
|
||||||
|
>
|
||||||
|
export type SerializedAutoLinkNode = SerializedLinkNode
|
||||||
@@ -15,9 +15,11 @@ import {
|
|||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import type { LinkFields } from '../../nodes/types'
|
||||||
|
|
||||||
import { invariant } from '../../../../lexical/utils/invariant'
|
import { invariant } from '../../../../lexical/utils/invariant'
|
||||||
import { $createAutoLinkNode, $isAutoLinkNode, AutoLinkNode } from '../../nodes/AutoLinkNode'
|
import { $createAutoLinkNode, $isAutoLinkNode, AutoLinkNode } from '../../nodes/AutoLinkNode'
|
||||||
import { $isLinkNode, type LinkFields } from '../../nodes/LinkNode'
|
import { $isLinkNode } from '../../nodes/LinkNode'
|
||||||
|
|
||||||
type ChangeHandler = (url: null | string, prevUrl: null | string) => void
|
type ChangeHandler = (url: null | string, prevUrl: null | string) => void
|
||||||
|
|
||||||
@@ -6,16 +6,7 @@ import { useModal } from '@faceless-ui/modal'
|
|||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import { $findMatchingParent, mergeRegister } from '@lexical/utils'
|
import { $findMatchingParent, mergeRegister } from '@lexical/utils'
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import {
|
import { formatDrawerSlug, useConfig, useEditDepth, useTranslation } from '@payloadcms/ui'
|
||||||
buildStateFromSchema,
|
|
||||||
formatDrawerSlug,
|
|
||||||
useAuth,
|
|
||||||
useConfig,
|
|
||||||
useDocumentInfo,
|
|
||||||
useEditDepth,
|
|
||||||
useLocale,
|
|
||||||
useTranslation,
|
|
||||||
} from '@payloadcms/ui'
|
|
||||||
import {
|
import {
|
||||||
$getSelection,
|
$getSelection,
|
||||||
$isRangeSelection,
|
$isRangeSelection,
|
||||||
@@ -24,29 +15,21 @@ import {
|
|||||||
KEY_ESCAPE_COMMAND,
|
KEY_ESCAPE_COMMAND,
|
||||||
SELECTION_CHANGE_COMMAND,
|
SELECTION_CHANGE_COMMAND,
|
||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
import { sanitizeFields } from 'payload/config'
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import type { LinkFeatureProps } from '../../..'
|
|
||||||
import type { LinkNode } from '../../../nodes/LinkNode'
|
import type { LinkNode } from '../../../nodes/LinkNode'
|
||||||
import type { LinkPayload } from '../types'
|
import type { LinkPayload } from '../types'
|
||||||
|
|
||||||
import { useEditorConfigContext } from '../../../../../lexical/config/EditorConfigProvider'
|
import { useEditorConfigContext } from '../../../../../lexical/config/client/EditorConfigProvider'
|
||||||
import { getSelectedNode } from '../../../../../lexical/utils/getSelectedNode'
|
import { getSelectedNode } from '../../../../../lexical/utils/getSelectedNode'
|
||||||
import { setFloatingElemPositionForLinkEditor } from '../../../../../lexical/utils/setFloatingElemPositionForLinkEditor'
|
import { setFloatingElemPositionForLinkEditor } from '../../../../../lexical/utils/setFloatingElemPositionForLinkEditor'
|
||||||
import { LinkDrawer } from '../../../drawer'
|
import { LinkDrawer } from '../../../drawer'
|
||||||
import { $isAutoLinkNode } from '../../../nodes/AutoLinkNode'
|
import { $isAutoLinkNode } from '../../../nodes/AutoLinkNode'
|
||||||
import { $createLinkNode } from '../../../nodes/LinkNode'
|
import { $createLinkNode } from '../../../nodes/LinkNode'
|
||||||
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '../../../nodes/LinkNode'
|
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '../../../nodes/LinkNode'
|
||||||
import { transformExtraFields } from '../utilities'
|
|
||||||
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './commands'
|
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './commands'
|
||||||
|
|
||||||
export function LinkEditor({
|
export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.ReactNode {
|
||||||
anchorElem,
|
|
||||||
disabledCollections,
|
|
||||||
enabledCollections,
|
|
||||||
fields: customFieldSchema,
|
|
||||||
}: { anchorElem: HTMLElement } & LinkFeatureProps): JSX.Element {
|
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
const editorRef = useRef<HTMLDivElement | null>(null)
|
const editorRef = useRef<HTMLDivElement | null>(null)
|
||||||
@@ -57,36 +40,9 @@ export function LinkEditor({
|
|||||||
|
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
|
|
||||||
const { user } = useAuth()
|
|
||||||
const { code: locale } = useLocale()
|
|
||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
|
|
||||||
const { getDocPreferences } = useDocumentInfo()
|
const [stateData, setStateData] = useState<LinkPayload>(null)
|
||||||
|
|
||||||
const [initialState, setInitialState] = useState<FormState>({})
|
|
||||||
|
|
||||||
const [fieldSchema] = useState(() => {
|
|
||||||
const fieldsUnsanitized = transformExtraFields(
|
|
||||||
customFieldSchema,
|
|
||||||
// TODO: fix this
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
config,
|
|
||||||
i18n,
|
|
||||||
enabledCollections,
|
|
||||||
disabledCollections,
|
|
||||||
)
|
|
||||||
// Sanitize custom fields here
|
|
||||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
|
||||||
const fields = sanitizeFields({
|
|
||||||
// TODO: fix this
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
config,
|
|
||||||
fields: fieldsUnsanitized,
|
|
||||||
validRelationships,
|
|
||||||
})
|
|
||||||
|
|
||||||
return fields
|
|
||||||
})
|
|
||||||
|
|
||||||
const { closeModal, toggleModal } = useModal()
|
const { closeModal, toggleModal } = useModal()
|
||||||
const editDepth = useEditDepth()
|
const editDepth = useEditDepth()
|
||||||
@@ -98,7 +54,7 @@ export function LinkEditor({
|
|||||||
depth: editDepth,
|
depth: editDepth,
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateLinkEditor = useCallback(async () => {
|
const updateLinkEditor = useCallback(() => {
|
||||||
const selection = $getSelection()
|
const selection = $getSelection()
|
||||||
let selectedNodeDomRect: DOMRect | undefined = null
|
let selectedNodeDomRect: DOMRect | undefined = null
|
||||||
|
|
||||||
@@ -147,22 +103,7 @@ export function LinkEditor({
|
|||||||
setLinkLabel(label)
|
setLinkLabel(label)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set initial state of the drawer. This will basically pre-fill the drawer fields with the
|
setStateData(data)
|
||||||
// values saved in the link node you clicked on.
|
|
||||||
const preferences = await getDocPreferences()
|
|
||||||
const state = await buildStateFromSchema({
|
|
||||||
// TODO: fix this
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
config,
|
|
||||||
data,
|
|
||||||
fieldSchema,
|
|
||||||
locale,
|
|
||||||
operation: 'create',
|
|
||||||
preferences,
|
|
||||||
t,
|
|
||||||
user: user ?? undefined,
|
|
||||||
})
|
|
||||||
setInitialState(state)
|
|
||||||
setIsLink(true)
|
setIsLink(true)
|
||||||
if ($isAutoLinkNode(linkParent)) {
|
if ($isAutoLinkNode(linkParent)) {
|
||||||
setIsAutoLink(true)
|
setIsAutoLink(true)
|
||||||
@@ -206,7 +147,7 @@ export function LinkEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}, [anchorElem, editor, fieldSchema, config, getDocPreferences, locale, t, user, i18n])
|
}, [anchorElem, editor, config, t, i18n])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return mergeRegister(
|
return mergeRegister(
|
||||||
@@ -217,12 +158,8 @@ export function LinkEditor({
|
|||||||
|
|
||||||
// Now, open the modal
|
// Now, open the modal
|
||||||
updateLinkEditor()
|
updateLinkEditor()
|
||||||
.then(() => {
|
|
||||||
toggleModal(drawerSlug)
|
toggleModal(drawerSlug)
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
throw error
|
|
||||||
})
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_LOW,
|
COMMAND_PRIORITY_LOW,
|
||||||
@@ -339,7 +276,6 @@ export function LinkEditor({
|
|||||||
</div>
|
</div>
|
||||||
<LinkDrawer
|
<LinkDrawer
|
||||||
drawerSlug={drawerSlug}
|
drawerSlug={drawerSlug}
|
||||||
fieldSchema={fieldSchema}
|
|
||||||
handleModalSubmit={(fields: FormState, data: Data) => {
|
handleModalSubmit={(fields: FormState, data: Data) => {
|
||||||
closeModal(drawerSlug)
|
closeModal(drawerSlug)
|
||||||
|
|
||||||
@@ -363,7 +299,7 @@ export function LinkEditor({
|
|||||||
// it being applied to the auto link node instead of the link node.
|
// it being applied to the auto link node instead of the link node.
|
||||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, newLinkPayload)
|
editor.dispatchCommand(TOGGLE_LINK_COMMAND, newLinkPayload)
|
||||||
}}
|
}}
|
||||||
initialState={initialState}
|
stateData={stateData}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '@payloadcms/ui/scss';
|
@import '../../../../../../../ui/src/scss/styles.scss';
|
||||||
|
|
||||||
html[data-theme='light'] {
|
html[data-theme='light'] {
|
||||||
.link-editor {
|
.link-editor {
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
'use client'
|
||||||
|
import * as React from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
|
import { LinkEditor } from './LinkEditor'
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
export const FloatingLinkEditorPlugin: React.FC<{
|
||||||
|
anchorElem: HTMLElement
|
||||||
|
}> = (props) => {
|
||||||
|
const { anchorElem = document.body } = props
|
||||||
|
|
||||||
|
return createPortal(<LinkEditor anchorElem={anchorElem} />, anchorElem)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { LinkFields } from '../../nodes/LinkNode'
|
import type { LinkFields } from '../../nodes/types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The payload of a link node
|
* The payload of a link node
|
||||||
@@ -10,10 +10,11 @@ import {
|
|||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import type { LinkFields } from '../../nodes/types'
|
||||||
import type { LinkPayload } from '../floatingLinkEditor/types'
|
import type { LinkPayload } from '../floatingLinkEditor/types'
|
||||||
|
|
||||||
import { validateUrl } from '../../../../lexical/utils/url'
|
import { validateUrl } from '../../../../lexical/utils/url'
|
||||||
import { type LinkFields, LinkNode, TOGGLE_LINK_COMMAND, toggleLink } from '../../nodes/LinkNode'
|
import { LinkNode, TOGGLE_LINK_COMMAND, toggleLink } from '../../nodes/LinkNode'
|
||||||
|
|
||||||
export function LinkPlugin(): null {
|
export function LinkPlugin(): null {
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { LinkFeatureProps } from '.'
|
|
||||||
import type { PopulationPromise } from '../types'
|
import type { PopulationPromise } from '../types'
|
||||||
import type { SerializedLinkNode } from './nodes/LinkNode'
|
import type { LinkFeatureServerProps } from './feature.server'
|
||||||
|
import type { SerializedLinkNode } from './nodes/types'
|
||||||
|
|
||||||
import { populate } from '../../../populate/populate'
|
import { populate } from '../../../populate/populate'
|
||||||
import { recurseNestedFields } from '../../../populate/recurseNestedFields'
|
import { recurseNestedFields } from '../../../populate/recurseNestedFields'
|
||||||
|
|
||||||
export const linkPopulationPromiseHOC = (
|
export const linkPopulationPromiseHOC = (
|
||||||
props: LinkFeatureProps,
|
props: LinkFeatureServerProps,
|
||||||
): PopulationPromise<SerializedLinkNode> => {
|
): PopulationPromise<SerializedLinkNode> => {
|
||||||
const linkPopulationPromise: PopulationPromise<SerializedLinkNode> = ({
|
const linkPopulationPromise: PopulationPromise<SerializedLinkNode> = ({
|
||||||
context,
|
context,
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import type { SerializedLinkNode } from '../../../../Link/nodes/LinkNode'
|
import type { SerializedLinkNode } from '../../../../link/nodes/LinkNode'
|
||||||
import type { LexicalPluginNodeConverter } from '../types'
|
import type { LexicalPluginNodeConverter } from '../types'
|
||||||
|
|
||||||
import { convertLexicalPluginNodesToLexical } from '..'
|
import { convertLexicalPluginNodesToLexical } from '..'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SerializedLinkNode } from '../../../../Link/nodes/LinkNode'
|
import type { SerializedLinkNode } from '../../../../link/nodes/LinkNode'
|
||||||
import type { SlateNodeConverter } from '../types'
|
import type { SlateNodeConverter } from '../types'
|
||||||
|
|
||||||
import { convertSlateNodesToLexical } from '..'
|
import { convertSlateNodesToLexical } from '..'
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import type { SerializedLexicalNode } from 'lexical'
|
|||||||
import type { LexicalNodeReplacement } from 'lexical'
|
import type { LexicalNodeReplacement } from 'lexical'
|
||||||
import type { RequestContext } from 'payload'
|
import type { RequestContext } from 'payload'
|
||||||
import type { SanitizedConfig } from 'payload/config'
|
import type { SanitizedConfig } from 'payload/config'
|
||||||
import type { PayloadRequest, RichTextField, ValidateOptions } from 'payload/types'
|
import type { Field, PayloadRequest, RichTextField, ValidateOptions } from 'payload/types'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
|
|
||||||
import type { AdapterProps } from '../../types'
|
import type { AdapterProps } from '../../types'
|
||||||
import type { EditorConfig } from '../lexical/config/types'
|
import type { ClientEditorConfig, ServerEditorConfig } from '../lexical/config/types'
|
||||||
import type { FloatingToolbarSection } from '../lexical/plugins/FloatingSelectToolbar/types'
|
import type { FloatingToolbarSection } from '../lexical/plugins/FloatingSelectToolbar/types'
|
||||||
import type { SlashMenuGroup } from '../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
import type { SlashMenuGroup } from '../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||||
import type { HTMLConverter } from './converters/html/converter/types'
|
import type { HTMLConverter } from './converters/html/converter/types'
|
||||||
@@ -62,10 +62,136 @@ export type NodeValidation<T extends SerializedLexicalNode = SerializedLexicalNo
|
|||||||
}
|
}
|
||||||
}) => Promise<string | true> | string | true
|
}) => Promise<string | true> | string | true
|
||||||
|
|
||||||
export type Feature = {
|
export type FeatureProviderProviderServer<ServerFeatureProps, ClientFeatureProps> = (
|
||||||
|
props?: ServerFeatureProps,
|
||||||
|
) => FeatureProviderServer<ServerFeatureProps, ClientFeatureProps>
|
||||||
|
|
||||||
|
export type FeatureProviderServer<ServerFeatureProps, ClientFeatureProps> = {
|
||||||
|
/** Keys of dependencies needed for this feature. These dependencies do not have to be loaded first */
|
||||||
|
dependencies?: string[]
|
||||||
|
/** Keys of priority dependencies needed for this feature. These dependencies have to be loaded first and are available in the `feature` property*/
|
||||||
|
dependenciesPriority?: string[]
|
||||||
|
/** Keys of soft-dependencies needed for this feature. The FeatureProviders dependencies are optional, but are considered as last-priority in the loading process */
|
||||||
|
dependenciesSoft?: string[]
|
||||||
|
|
||||||
|
feature: (props: {
|
||||||
|
/** unSanitizedEditorConfig.features, but mapped */
|
||||||
|
featureProviderMap: ServerFeatureProviderMap
|
||||||
|
// other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here
|
||||||
|
resolvedFeatures: ResolvedServerFeatureMap
|
||||||
|
// unSanitized EditorConfig,
|
||||||
|
unSanitizedEditorConfig: ServerEditorConfig
|
||||||
|
}) => ServerFeature<ServerFeatureProps, ClientFeatureProps>
|
||||||
|
key: string
|
||||||
|
/** Props which were passed into your feature will have to be passed here. This will allow them to be used / read in other places of the code, e.g. wherever you can use useEditorConfigContext */
|
||||||
|
serverFeatureProps: ServerFeatureProps
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FeatureProviderProviderClient<ClientFeatureProps> = (
|
||||||
|
props?: ClientComponentProps<ClientFeatureProps>,
|
||||||
|
) => FeatureProviderClient<ClientFeatureProps>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No dependencies => Features need to be sorted on the server first, then sent to client in right order
|
||||||
|
*/
|
||||||
|
export type FeatureProviderClient<ClientFeatureProps> = {
|
||||||
|
/**
|
||||||
|
* Return props, to make it easy to retrieve passed in props to this Feature for the client if anyone wants to
|
||||||
|
*/
|
||||||
|
clientFeatureProps: ClientComponentProps<ClientFeatureProps>
|
||||||
|
feature: (props: {
|
||||||
|
/** unSanitizedEditorConfig.features, but mapped */
|
||||||
|
featureProviderMap: ClientFeatureProviderMap
|
||||||
|
// other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here
|
||||||
|
resolvedFeatures: ResolvedClientFeatureMap
|
||||||
|
// unSanitized EditorConfig,
|
||||||
|
unSanitizedEditorConfig: ClientEditorConfig
|
||||||
|
}) => ClientFeature<ClientFeatureProps>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientFeature<ClientFeatureProps> = {
|
||||||
|
/**
|
||||||
|
* Return props, to make it easy to retrieve passed in props to this Feature for the client if anyone wants to
|
||||||
|
*/
|
||||||
|
clientFeatureProps: ClientComponentProps<ClientFeatureProps>
|
||||||
|
|
||||||
floatingSelectToolbar?: {
|
floatingSelectToolbar?: {
|
||||||
sections: FloatingToolbarSection[]
|
sections: FloatingToolbarSection[]
|
||||||
}
|
}
|
||||||
|
hooks?: {
|
||||||
|
load?: ({
|
||||||
|
incomingEditorState,
|
||||||
|
}: {
|
||||||
|
incomingEditorState: SerializedEditorState
|
||||||
|
}) => SerializedEditorState
|
||||||
|
save?: ({
|
||||||
|
incomingEditorState,
|
||||||
|
}: {
|
||||||
|
incomingEditorState: SerializedEditorState
|
||||||
|
}) => SerializedEditorState
|
||||||
|
}
|
||||||
|
markdownTransformers?: Transformer[]
|
||||||
|
nodes?: Array<Klass<LexicalNode> | LexicalNodeReplacement>
|
||||||
|
plugins?: Array<
|
||||||
|
| {
|
||||||
|
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
||||||
|
Component: React.FC
|
||||||
|
position: 'bottom' // Determines at which position the Component will be added.
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
||||||
|
Component: React.FC
|
||||||
|
position: 'normal' // Determines at which position the Component will be added.
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
||||||
|
Component: React.FC
|
||||||
|
position: 'top' // Determines at which position the Component will be added.
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
||||||
|
Component: React.FC<{ anchorElem: HTMLElement }>
|
||||||
|
position: 'floatingAnchorElem' // Determines at which position the Component will be added.
|
||||||
|
}
|
||||||
|
>
|
||||||
|
slashMenu?: {
|
||||||
|
dynamicOptions?: ({
|
||||||
|
editor,
|
||||||
|
queryString,
|
||||||
|
}: {
|
||||||
|
editor: LexicalEditor
|
||||||
|
queryString: string
|
||||||
|
}) => SlashMenuGroup[]
|
||||||
|
options?: SlashMenuGroup[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientComponentProps<ClientFeatureProps> = ClientFeatureProps & {
|
||||||
|
featureKey: string
|
||||||
|
order: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerFeature<ServerProps, ClientFeatureProps> = {
|
||||||
|
ClientComponent: React.FC<ClientComponentProps<ClientFeatureProps>>
|
||||||
|
/**
|
||||||
|
* This determines what props will be available on the Client.
|
||||||
|
*/
|
||||||
|
clientFeatureProps: ClientFeatureProps
|
||||||
|
generateComponentMap?: (args: {
|
||||||
|
config: SanitizedConfig
|
||||||
|
props: ServerProps
|
||||||
|
schemaPath: string
|
||||||
|
}) => {
|
||||||
|
[key: string]: React.FC
|
||||||
|
}
|
||||||
|
generateSchemaMap?: (args: {
|
||||||
|
config: SanitizedConfig
|
||||||
|
props: ServerProps
|
||||||
|
schemaMap: Map<string, Field[]>
|
||||||
|
schemaPath: string
|
||||||
|
}) => {
|
||||||
|
[key: string]: Field[]
|
||||||
|
}
|
||||||
generatedTypes?: {
|
generatedTypes?: {
|
||||||
modifyOutputSchema: ({
|
modifyOutputSchema: ({
|
||||||
currentSchema,
|
currentSchema,
|
||||||
@@ -95,16 +221,6 @@ export type Feature = {
|
|||||||
incomingEditorState: SerializedEditorState
|
incomingEditorState: SerializedEditorState
|
||||||
siblingDoc: Record<string, unknown>
|
siblingDoc: Record<string, unknown>
|
||||||
}) => Promise<void> | null
|
}) => Promise<void> | null
|
||||||
load?: ({
|
|
||||||
incomingEditorState,
|
|
||||||
}: {
|
|
||||||
incomingEditorState: SerializedEditorState
|
|
||||||
}) => SerializedEditorState
|
|
||||||
save?: ({
|
|
||||||
incomingEditorState,
|
|
||||||
}: {
|
|
||||||
incomingEditorState: SerializedEditorState
|
|
||||||
}) => SerializedEditorState
|
|
||||||
}
|
}
|
||||||
markdownTransformers?: Transformer[]
|
markdownTransformers?: Transformer[]
|
||||||
nodes?: Array<{
|
nodes?: Array<{
|
||||||
@@ -113,107 +229,66 @@ export type Feature = {
|
|||||||
}
|
}
|
||||||
node: Klass<LexicalNode> | LexicalNodeReplacement
|
node: Klass<LexicalNode> | LexicalNodeReplacement
|
||||||
populationPromises?: Array<PopulationPromise>
|
populationPromises?: Array<PopulationPromise>
|
||||||
type: string
|
|
||||||
validations?: Array<NodeValidation>
|
validations?: Array<NodeValidation>
|
||||||
}>
|
}>
|
||||||
plugins?: Array<
|
|
||||||
| {
|
|
||||||
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
|
||||||
Component: () => Promise<React.FC<{ anchorElem: HTMLElement }>>
|
|
||||||
position: 'floatingAnchorElem' // Determines at which position the Component will be added.
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
|
||||||
Component: () => Promise<React.FC>
|
|
||||||
position: 'bottom' // Determines at which position the Component will be added.
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
|
||||||
Component: () => Promise<React.FC>
|
|
||||||
position: 'normal' // Determines at which position the Component will be added.
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
|
||||||
Component: () => Promise<React.FC>
|
|
||||||
position: 'top' // Determines at which position the Component will be added.
|
|
||||||
}
|
|
||||||
>
|
|
||||||
|
|
||||||
/** Props which were passed into your feature will have to be passed here. This will allow them to be used / read in other places of the code, e.g. wherever you can use useEditorConfigContext */
|
/** Props which were passed into your feature will have to be passed here. This will allow them to be used / read in other places of the code, e.g. wherever you can use useEditorConfigContext */
|
||||||
props: unknown
|
serverFeatureProps: ServerProps
|
||||||
slashMenu?: {
|
|
||||||
dynamicOptions?: ({
|
|
||||||
editor,
|
|
||||||
queryString,
|
|
||||||
}: {
|
|
||||||
editor: LexicalEditor
|
|
||||||
queryString: string
|
|
||||||
}) => SlashMenuGroup[]
|
|
||||||
options?: SlashMenuGroup[]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FeatureProvider = {
|
export type ResolvedServerFeature<ServerProps, ClientFeatureProps> = ServerFeature<
|
||||||
Component: React.FC
|
ServerProps,
|
||||||
/** Keys of dependencies needed for this feature. These dependencies do not have to be loaded first */
|
ClientFeatureProps
|
||||||
dependencies?: string[]
|
> &
|
||||||
/** Keys of priority dependencies needed for this feature. These dependencies have to be loaded first and are available in the `feature` property*/
|
|
||||||
dependenciesPriority?: string[]
|
|
||||||
|
|
||||||
/** Keys of soft-dependencies needed for this feature. These dependencies are optional, but are considered as last-priority in the loading process */
|
|
||||||
dependenciesSoft?: string[]
|
|
||||||
feature: (props: {
|
|
||||||
/** unSanitizedEditorConfig.features, but mapped */
|
|
||||||
featureProviderMap: FeatureProviderMap
|
|
||||||
// other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here
|
|
||||||
resolvedFeatures: ResolvedFeatureMap
|
|
||||||
// unSanitized EditorConfig,
|
|
||||||
unSanitizedEditorConfig: EditorConfig
|
|
||||||
}) => Feature
|
|
||||||
key: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ResolvedFeature = Feature &
|
|
||||||
Required<
|
Required<
|
||||||
Pick<
|
Pick<
|
||||||
FeatureProvider,
|
FeatureProviderServer<ServerProps, ClientFeatureProps>,
|
||||||
'Component' | 'dependencies' | 'dependenciesPriority' | 'dependenciesSoft' | 'key'
|
'dependencies' | 'dependenciesPriority' | 'dependenciesSoft' | 'key'
|
||||||
>
|
|
||||||
>
|
>
|
||||||
|
> & {
|
||||||
|
order: number
|
||||||
|
}
|
||||||
|
|
||||||
export type ResolvedFeatureMap = Map<string, ResolvedFeature>
|
export type ResolvedClientFeature<ClientFeatureProps> = ClientFeature<ClientFeatureProps> & {
|
||||||
|
key: string
|
||||||
|
order: number
|
||||||
|
}
|
||||||
|
|
||||||
export type FeatureProviderMap = Map<string, FeatureProvider>
|
export type ResolvedServerFeatureMap = Map<string, ResolvedServerFeature<unknown, unknown>>
|
||||||
|
export type ResolvedClientFeatureMap = Map<string, ResolvedClientFeature<unknown>>
|
||||||
|
|
||||||
|
export type ServerFeatureProviderMap = Map<string, FeatureProviderServer<unknown, unknown>>
|
||||||
|
export type ClientFeatureProviderMap = Map<string, FeatureProviderClient<unknown>>
|
||||||
|
|
||||||
export type SanitizedPlugin =
|
export type SanitizedPlugin =
|
||||||
| {
|
| {
|
||||||
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
||||||
Component: () => Promise<React.FC<{ anchorElem: HTMLElement }>>
|
Component: React.FC
|
||||||
desktopOnly?: boolean
|
|
||||||
key: string
|
|
||||||
position: 'floatingAnchorElem' // Determines at which position the Component will be added.
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
|
||||||
Component: () => Promise<React.FC>
|
|
||||||
key: string
|
key: string
|
||||||
position: 'bottom' // Determines at which position the Component will be added.
|
position: 'bottom' // Determines at which position the Component will be added.
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
||||||
Component: () => Promise<React.FC>
|
Component: React.FC
|
||||||
key: string
|
key: string
|
||||||
position: 'normal' // Determines at which position the Component will be added.
|
position: 'normal' // Determines at which position the Component will be added.
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
||||||
Component: () => Promise<React.FC>
|
Component: React.FC
|
||||||
key: string
|
key: string
|
||||||
position: 'top' // Determines at which position the Component will be added.
|
position: 'top' // Determines at which position the Component will be added.
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
||||||
|
Component: React.FC<{ anchorElem: HTMLElement }>
|
||||||
|
desktopOnly?: boolean
|
||||||
|
key: string
|
||||||
|
position: 'floatingAnchorElem' // Determines at which position the Component will be added.
|
||||||
|
}
|
||||||
|
|
||||||
export type SanitizedFeatures = Required<
|
export type SanitizedServerFeatures = Required<
|
||||||
Pick<ResolvedFeature, 'markdownTransformers' | 'nodes'>
|
Pick<ResolvedServerFeature<unknown, unknown>, 'markdownTransformers' | 'nodes'>
|
||||||
> & {
|
> & {
|
||||||
/** The node types mapped to their converters */
|
/** The node types mapped to their converters */
|
||||||
converters: {
|
converters: {
|
||||||
@@ -221,9 +296,6 @@ export type SanitizedFeatures = Required<
|
|||||||
}
|
}
|
||||||
/** The keys of all enabled features */
|
/** The keys of all enabled features */
|
||||||
enabledFeatures: string[]
|
enabledFeatures: string[]
|
||||||
floatingSelectToolbar: {
|
|
||||||
sections: FloatingToolbarSection[]
|
|
||||||
}
|
|
||||||
generatedTypes: {
|
generatedTypes: {
|
||||||
modifyOutputSchemas: Array<
|
modifyOutputSchemas: Array<
|
||||||
({
|
({
|
||||||
@@ -257,6 +329,22 @@ export type SanitizedFeatures = Required<
|
|||||||
siblingDoc: Record<string, unknown>
|
siblingDoc: Record<string, unknown>
|
||||||
}) => Promise<void> | null
|
}) => Promise<void> | null
|
||||||
>
|
>
|
||||||
|
}
|
||||||
|
/** The node types mapped to their populationPromises */
|
||||||
|
populationPromises: Map<string, Array<PopulationPromise>>
|
||||||
|
/** The node types mapped to their validations */
|
||||||
|
validations: Map<string, Array<NodeValidation>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SanitizedClientFeatures = Required<
|
||||||
|
Pick<ResolvedClientFeature<unknown>, 'markdownTransformers' | 'nodes'>
|
||||||
|
> & {
|
||||||
|
/** The keys of all enabled features */
|
||||||
|
enabledFeatures: string[]
|
||||||
|
floatingSelectToolbar: {
|
||||||
|
sections: FloatingToolbarSection[]
|
||||||
|
}
|
||||||
|
hooks: {
|
||||||
load: Array<
|
load: Array<
|
||||||
({
|
({
|
||||||
incomingEditorState,
|
incomingEditorState,
|
||||||
@@ -273,14 +361,10 @@ export type SanitizedFeatures = Required<
|
|||||||
>
|
>
|
||||||
}
|
}
|
||||||
plugins?: Array<SanitizedPlugin>
|
plugins?: Array<SanitizedPlugin>
|
||||||
/** The node types mapped to their populationPromises */
|
|
||||||
populationPromises: Map<string, Array<PopulationPromise>>
|
|
||||||
slashMenu: {
|
slashMenu: {
|
||||||
dynamicOptions: Array<
|
dynamicOptions: Array<
|
||||||
({ editor, queryString }: { editor: LexicalEditor; queryString: string }) => SlashMenuGroup[]
|
({ editor, queryString }: { editor: LexicalEditor; queryString: string }) => SlashMenuGroup[]
|
||||||
>
|
>
|
||||||
groupsWithOptions: SlashMenuGroup[]
|
groupsWithOptions: SlashMenuGroup[]
|
||||||
}
|
}
|
||||||
/** The node types mapped to their validations */
|
|
||||||
validations: Map<string, Array<NodeValidation>>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '@payloadcms/ui/scss';
|
@import '../../../ui/src/scss/styles.scss';
|
||||||
|
|
||||||
.rich-text-lexical {
|
.rich-text-lexical {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FeatureProvider } from '@payloadcms/richtext-lexical'
|
|
||||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||||
|
|
||||||
import { type FormFieldBase, ShimmerEffect } from '@payloadcms/ui'
|
import { type FormFieldBase, ShimmerEffect } from '@payloadcms/ui'
|
||||||
import React, { Suspense, lazy, useEffect, useState } from 'react'
|
import React, { Suspense, lazy, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import type { SanitizedEditorConfig } from './lexical/config/types'
|
import type { GeneratedFeatureProviderComponent } from '../types'
|
||||||
|
import type { FeatureProviderClient } from './features/types'
|
||||||
|
import type { SanitizedClientEditorConfig } from './lexical/config/types'
|
||||||
|
|
||||||
import { useFieldPath } from '../../../ui/src/forms/FieldPathProvider'
|
import { useFieldPath } from '../../../ui/src/forms/FieldPathProvider'
|
||||||
import { useClientFunctions } from '../../../ui/src/providers/ClientFunction'
|
import { useClientFunctions } from '../../../ui/src/providers/ClientFunction'
|
||||||
import { defaultEditorLexicalConfig } from './lexical/config/defaultClient'
|
import { defaultEditorLexicalConfig } from './lexical/config/client/default'
|
||||||
import { sanitizeEditorConfig } from './lexical/config/sanitize'
|
import { loadClientFeatures } from './lexical/config/client/loader'
|
||||||
|
import { sanitizeClientEditorConfig } from './lexical/config/client/sanitize'
|
||||||
|
|
||||||
// @ts-expect-error-next-line Just TypeScript being broken // TODO: Open TypeScript issue
|
// @ts-expect-error-next-line Just TypeScript being broken // TODO: Open TypeScript issue
|
||||||
const RichTextEditor = lazy(() => import('./Field'))
|
const RichTextEditor = lazy(() => import('./Field'))
|
||||||
@@ -27,20 +29,19 @@ export const RichTextField: React.FC<
|
|||||||
const clientFunctions = useClientFunctions()
|
const clientFunctions = useClientFunctions()
|
||||||
const [hasLoadedFeatures, setHasLoadedFeatures] = useState(false)
|
const [hasLoadedFeatures, setHasLoadedFeatures] = useState(false)
|
||||||
|
|
||||||
const [featureProviders, setFeatureProviders] = useState<FeatureProvider[]>([])
|
const [featureProviders, setFeatureProviders] = useState<FeatureProviderClient<unknown>[]>([])
|
||||||
|
|
||||||
const finalSanitizedEditorConfig: SanitizedEditorConfig = sanitizeEditorConfig({
|
const [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] =
|
||||||
features: [],
|
useState<SanitizedClientEditorConfig>(null)
|
||||||
lexical: lexicalEditorConfig
|
|
||||||
? () => Promise.resolve(lexicalEditorConfig)
|
|
||||||
: () => Promise.resolve(defaultEditorLexicalConfig),
|
|
||||||
})
|
|
||||||
|
|
||||||
const featureProviderComponents = Array.from(richTextComponentMap.values())
|
let featureProviderComponents: GeneratedFeatureProviderComponent[] =
|
||||||
|
richTextComponentMap.get('features')
|
||||||
|
// order by order
|
||||||
|
featureProviderComponents = featureProviderComponents.sort((a, b) => a.order - b.order)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasLoadedFeatures) {
|
if (!hasLoadedFeatures) {
|
||||||
const featureProvidersLocal: FeatureProvider[] = []
|
const featureProvidersLocal: FeatureProviderClient<unknown>[] = []
|
||||||
|
|
||||||
Object.entries(clientFunctions).forEach(([key, plugin]) => {
|
Object.entries(clientFunctions).forEach(([key, plugin]) => {
|
||||||
if (key.startsWith(`lexicalFeature.${schemaPath}.`)) {
|
if (key.startsWith(`lexicalFeature.${schemaPath}.`)) {
|
||||||
@@ -51,30 +52,56 @@ export const RichTextField: React.FC<
|
|||||||
if (featureProvidersLocal.length === featureProviderComponents.length) {
|
if (featureProvidersLocal.length === featureProviderComponents.length) {
|
||||||
setFeatureProviders(featureProvidersLocal)
|
setFeatureProviders(featureProvidersLocal)
|
||||||
setHasLoadedFeatures(true)
|
setHasLoadedFeatures(true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loaded feature provided => create the final sanitized editor config
|
||||||
|
*/
|
||||||
|
|
||||||
|
const resolvedClientFeatures = loadClientFeatures({
|
||||||
|
unSanitizedEditorConfig: {
|
||||||
|
features: featureProvidersLocal,
|
||||||
|
lexical: lexicalEditorConfig,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
setFinalSanitizedEditorConfig(
|
||||||
|
sanitizeClientEditorConfig(
|
||||||
|
lexicalEditorConfig ? lexicalEditorConfig : defaultEditorLexicalConfig,
|
||||||
|
resolvedClientFeatures,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [hasLoadedFeatures, clientFunctions, schemaPath, featureProviderComponents.length])
|
}, [
|
||||||
|
hasLoadedFeatures,
|
||||||
|
clientFunctions,
|
||||||
|
schemaPath,
|
||||||
|
featureProviderComponents.length,
|
||||||
|
featureProviders,
|
||||||
|
finalSanitizedEditorConfig,
|
||||||
|
lexicalEditorConfig,
|
||||||
|
])
|
||||||
|
|
||||||
if (!hasLoadedFeatures) {
|
if (!hasLoadedFeatures) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{Array.isArray(featureProviderComponents) &&
|
{Array.isArray(featureProviderComponents) &&
|
||||||
featureProviderComponents.map((FeatureProvider, i) => {
|
featureProviderComponents.map((FeatureProvider) => {
|
||||||
return <React.Fragment key={i}>{FeatureProvider}</React.Fragment>
|
return (
|
||||||
|
<React.Fragment key={FeatureProvider.key}>
|
||||||
|
{FeatureProvider.ClientComponent}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
})}
|
})}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const features = clientFunctions
|
|
||||||
|
|
||||||
console.log('clientFunctions', features['lexicalFeature.posts.richText.paragraph'])
|
|
||||||
|
|
||||||
//features['lexicalFeature.posts.richText.paragraph']()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<ShimmerEffect height="35vh" />}>
|
<Suspense fallback={<ShimmerEffect height="35vh" />}>
|
||||||
|
{finalSanitizedEditorConfig && (
|
||||||
<RichTextEditor {...props} editorConfig={finalSanitizedEditorConfig} />
|
<RichTextEditor {...props} editorConfig={finalSanitizedEditorConfig} />
|
||||||
|
)}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useMemo } from 'react'
|
|
||||||
|
|
||||||
import type { SanitizedPlugin } from '../features/types'
|
import type { SanitizedPlugin } from '../features/types'
|
||||||
|
|
||||||
@@ -7,31 +6,9 @@ export const EditorPlugin: React.FC<{
|
|||||||
anchorElem?: HTMLDivElement
|
anchorElem?: HTMLDivElement
|
||||||
plugin: SanitizedPlugin
|
plugin: SanitizedPlugin
|
||||||
}> = ({ anchorElem, plugin }) => {
|
}> = ({ anchorElem, plugin }) => {
|
||||||
const Component: React.FC<any> = useMemo(() => {
|
|
||||||
return plugin?.Component
|
|
||||||
? React.lazy(() =>
|
|
||||||
plugin.Component().then((resolvedComponent) => ({
|
|
||||||
default: resolvedComponent,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
}, [plugin]) // Dependency array ensures this is only recomputed if entry changes
|
|
||||||
|
|
||||||
if (plugin.position === 'floatingAnchorElem') {
|
if (plugin.position === 'floatingAnchorElem') {
|
||||||
return (
|
return plugin.Component && <plugin.Component anchorElem={anchorElem} />
|
||||||
Component && (
|
|
||||||
<React.Suspense>
|
|
||||||
<Component anchorElem={anchorElem} />
|
|
||||||
</React.Suspense>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return plugin.Component && <plugin.Component />
|
||||||
Component && (
|
|
||||||
<React.Suspense>
|
|
||||||
<Component />
|
|
||||||
</React.Suspense>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
|
|||||||
{editor.isEditable() && (
|
{editor.isEditable() && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<MarkdownShortcutPlugin />
|
{editorConfig?.features?.markdownTransformers?.length > 0 && <MarkdownShortcutPlugin />}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { InitialConfigType } from '@lexical/react/LexicalComposer'
|
import type { InitialConfigType } from '@lexical/react/LexicalComposer'
|
||||||
|
import type { FormFieldBase } from '@payloadcms/ui'
|
||||||
import type { EditorState, SerializedEditorState } from 'lexical'
|
import type { EditorState, SerializedEditorState } from 'lexical'
|
||||||
import type { LexicalEditor } from 'lexical'
|
import type { LexicalEditor } from 'lexical'
|
||||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
|
||||||
|
|
||||||
import { LexicalComposer } from '@lexical/react/LexicalComposer'
|
import { LexicalComposer } from '@lexical/react/LexicalComposer'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import type { FieldProps } from '../../types'
|
import type { SanitizedClientEditorConfig } from './config/types'
|
||||||
import type { SanitizedEditorConfig } from './config/types'
|
|
||||||
|
|
||||||
import { LexicalEditor as LexicalEditorComponent } from './LexicalEditor'
|
import { LexicalEditor as LexicalEditorComponent } from './LexicalEditor'
|
||||||
import { EditorConfigProvider } from './config/EditorConfigProvider'
|
import { EditorConfigProvider } from './config/client/EditorConfigProvider'
|
||||||
import { getEnabledNodes } from './nodes'
|
import { getEnabledNodes } from './nodes'
|
||||||
|
|
||||||
export type LexicalProviderProps = {
|
export type LexicalProviderProps = {
|
||||||
editorConfig: SanitizedEditorConfig
|
editorConfig: SanitizedClientEditorConfig
|
||||||
fieldProps: FieldProps
|
fieldProps: FormFieldBase & {
|
||||||
|
editorConfig: SanitizedClientEditorConfig // With rendered features n stuff
|
||||||
|
name: string
|
||||||
|
richTextComponentMap: Map<string, React.ReactNode>
|
||||||
|
}
|
||||||
onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set<string>) => void
|
onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set<string>) => void
|
||||||
path: string
|
path: string
|
||||||
readOnly: boolean
|
readOnly: boolean
|
||||||
@@ -27,21 +31,19 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
|
|||||||
|
|
||||||
const [initialConfig, setInitialConfig] = React.useState<InitialConfigType | null>(null)
|
const [initialConfig, setInitialConfig] = React.useState<InitialConfigType | null>(null)
|
||||||
|
|
||||||
// set lexical config in useffect async:
|
// set lexical config in useEffect: // TODO: Is this the most performant way to do this? Prob not
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
void editorConfig.lexical().then((lexicalConfig: LexicalEditorConfig) => {
|
|
||||||
const newInitialConfig: InitialConfigType = {
|
const newInitialConfig: InitialConfigType = {
|
||||||
editable: readOnly !== true,
|
editable: readOnly !== true,
|
||||||
editorState: value != null ? JSON.stringify(value) : undefined,
|
editorState: value != null ? JSON.stringify(value) : undefined,
|
||||||
namespace: lexicalConfig.namespace,
|
namespace: editorConfig.lexical.namespace,
|
||||||
nodes: [...getEnabledNodes({ editorConfig })],
|
nodes: [...getEnabledNodes({ editorConfig })],
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
throw error
|
throw error
|
||||||
},
|
},
|
||||||
theme: lexicalConfig.theme,
|
theme: editorConfig.lexical.theme,
|
||||||
}
|
}
|
||||||
setInitialConfig(newInitialConfig)
|
setInitialConfig(newInitialConfig)
|
||||||
})
|
|
||||||
}, [editorConfig, readOnly, value])
|
}, [editorConfig, readOnly, value])
|
||||||
|
|
||||||
if (editorConfig?.features?.hooks?.load?.length) {
|
if (editorConfig?.features?.hooks?.load?.length) {
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
import type { FormFieldBase } from '@payloadcms/ui'
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import type { FieldProps } from '../../../types'
|
import type { FieldProps } from '../../../../types'
|
||||||
import type { SanitizedEditorConfig } from './types'
|
import type { SanitizedClientEditorConfig } from '../types'
|
||||||
|
|
||||||
// Should always produce a 20 character pseudo-random string
|
// Should always produce a 20 character pseudo-random string
|
||||||
function generateQuickGuid(): string {
|
function generateQuickGuid(): string {
|
||||||
return Math.random().toString(36).substring(2, 12) + Math.random().toString(36).substring(2, 12)
|
return Math.random().toString(36).substring(2, 12) + Math.random().toString(36).substring(2, 12)
|
||||||
}
|
}
|
||||||
interface ContextType {
|
interface ContextType {
|
||||||
editorConfig: SanitizedEditorConfig
|
editorConfig: SanitizedClientEditorConfig
|
||||||
field: FieldProps
|
field: FormFieldBase & {
|
||||||
|
editorConfig: SanitizedClientEditorConfig // With rendered features n stuff
|
||||||
|
name: string
|
||||||
|
richTextComponentMap: Map<string, React.ReactNode>
|
||||||
|
}
|
||||||
uuid: string
|
uuid: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,9 +33,13 @@ export const EditorConfigProvider = ({
|
|||||||
fieldProps,
|
fieldProps,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
editorConfig: SanitizedEditorConfig
|
editorConfig: SanitizedClientEditorConfig
|
||||||
fieldProps: FieldProps
|
fieldProps: FormFieldBase & {
|
||||||
}): JSX.Element => {
|
editorConfig: SanitizedClientEditorConfig // With rendered features n stuff
|
||||||
|
name: string
|
||||||
|
richTextComponentMap: Map<string, React.ReactNode>
|
||||||
|
}
|
||||||
|
}): React.ReactNode => {
|
||||||
// State to store the UUID
|
// State to store the UUID
|
||||||
const [uuid, setUuid] = useState(generateQuickGuid())
|
const [uuid, setUuid] = useState(generateQuickGuid())
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
'use client'
|
||||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||||
|
|
||||||
import { LexicalEditorTheme } from '../theme/EditorTheme'
|
import { LexicalEditorTheme } from '../../theme/EditorTheme'
|
||||||
|
|
||||||
export const defaultEditorLexicalConfig: LexicalEditorConfig = {
|
export const defaultEditorLexicalConfig: LexicalEditorConfig = {
|
||||||
namespace: 'lexical',
|
namespace: 'lexical',
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ClientEditorConfig,
|
||||||
|
ClientFeatureProviderMap,
|
||||||
|
FeatureProviderClient,
|
||||||
|
ResolvedClientFeatureMap,
|
||||||
|
} from '@payloadcms/richtext-lexical'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function expects client functions to ALREADY be ordered & dependencies checked on the server
|
||||||
|
* @param unSanitizedEditorConfig
|
||||||
|
*/
|
||||||
|
export function loadClientFeatures({
|
||||||
|
unSanitizedEditorConfig,
|
||||||
|
}: {
|
||||||
|
unSanitizedEditorConfig: ClientEditorConfig
|
||||||
|
}): ResolvedClientFeatureMap {
|
||||||
|
for (const featureProvider of unSanitizedEditorConfig.features) {
|
||||||
|
if (
|
||||||
|
!featureProvider?.clientFeatureProps?.featureKey ||
|
||||||
|
featureProvider?.clientFeatureProps?.order === undefined ||
|
||||||
|
featureProvider?.clientFeatureProps?.order === null
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'A Feature you have installed does not return the client props as clientFeatureProps. Please make sure to always return those props, even if they are null, as other important props like order and featureKey are later on injected.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort unSanitizedEditorConfig.features by order
|
||||||
|
unSanitizedEditorConfig.features = unSanitizedEditorConfig.features.sort(
|
||||||
|
(a, b) => a.clientFeatureProps.order - b.clientFeatureProps.order,
|
||||||
|
)
|
||||||
|
|
||||||
|
const featureProviderMap: ClientFeatureProviderMap = new Map(
|
||||||
|
unSanitizedEditorConfig.features.map(
|
||||||
|
(f) => [f.clientFeatureProps.featureKey, f] as [string, FeatureProviderClient<unknown>],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolvedFeatures: ResolvedClientFeatureMap = new Map()
|
||||||
|
|
||||||
|
// Make sure all dependencies declared in the respective features exist
|
||||||
|
let loaded = 0
|
||||||
|
for (const featureProvider of unSanitizedEditorConfig.features) {
|
||||||
|
const feature = featureProvider.feature({
|
||||||
|
featureProviderMap,
|
||||||
|
resolvedFeatures,
|
||||||
|
unSanitizedEditorConfig,
|
||||||
|
})
|
||||||
|
resolvedFeatures.set(featureProvider.clientFeatureProps.featureKey, {
|
||||||
|
...feature,
|
||||||
|
key: featureProvider.clientFeatureProps.featureKey,
|
||||||
|
order: loaded,
|
||||||
|
})
|
||||||
|
|
||||||
|
loaded++
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedFeatures
|
||||||
|
}
|
||||||
@@ -1,46 +1,37 @@
|
|||||||
import type { ResolvedFeatureMap, SanitizedFeatures } from '../../features/types'
|
'use client'
|
||||||
import type { EditorConfig, SanitizedEditorConfig } from './types'
|
|
||||||
|
|
||||||
import { loadFeatures } from './loader'
|
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||||
|
|
||||||
export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeatures => {
|
import type { ResolvedClientFeatureMap, SanitizedClientFeatures } from '../../../features/types'
|
||||||
const sanitized: SanitizedFeatures = {
|
import type { SanitizedClientEditorConfig } from '../types'
|
||||||
converters: {
|
|
||||||
html: [],
|
export const sanitizeClientFeatures = (
|
||||||
},
|
features: ResolvedClientFeatureMap,
|
||||||
|
): SanitizedClientFeatures => {
|
||||||
|
const sanitized: SanitizedClientFeatures = {
|
||||||
enabledFeatures: [],
|
enabledFeatures: [],
|
||||||
floatingSelectToolbar: {
|
floatingSelectToolbar: {
|
||||||
sections: [],
|
sections: [],
|
||||||
},
|
},
|
||||||
generatedTypes: {
|
|
||||||
modifyOutputSchemas: [],
|
|
||||||
},
|
|
||||||
hooks: {
|
hooks: {
|
||||||
afterReadPromises: [],
|
|
||||||
load: [],
|
load: [],
|
||||||
save: [],
|
save: [],
|
||||||
},
|
},
|
||||||
markdownTransformers: [],
|
|
||||||
nodes: [],
|
nodes: [],
|
||||||
plugins: [],
|
plugins: [],
|
||||||
populationPromises: new Map(),
|
|
||||||
slashMenu: {
|
slashMenu: {
|
||||||
dynamicOptions: [],
|
dynamicOptions: [],
|
||||||
groupsWithOptions: [],
|
groupsWithOptions: [],
|
||||||
},
|
},
|
||||||
validations: new Map(),
|
}
|
||||||
|
|
||||||
|
if (!features?.size) {
|
||||||
|
return sanitized
|
||||||
}
|
}
|
||||||
|
|
||||||
features.forEach((feature) => {
|
features.forEach((feature) => {
|
||||||
if (feature?.generatedTypes?.modifyOutputSchema) {
|
|
||||||
sanitized.generatedTypes.modifyOutputSchemas.push(feature.generatedTypes.modifyOutputSchema)
|
|
||||||
}
|
|
||||||
if (feature.hooks) {
|
if (feature.hooks) {
|
||||||
if (feature.hooks.afterReadPromise) {
|
|
||||||
sanitized.hooks.afterReadPromises = sanitized.hooks.afterReadPromises.concat(
|
|
||||||
feature.hooks.afterReadPromise,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (feature.hooks?.load?.length) {
|
if (feature.hooks?.load?.length) {
|
||||||
sanitized.hooks.load = sanitized.hooks.load.concat(feature.hooks.load)
|
sanitized.hooks.load = sanitized.hooks.load.concat(feature.hooks.load)
|
||||||
}
|
}
|
||||||
@@ -51,17 +42,6 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
|||||||
|
|
||||||
if (feature.nodes?.length) {
|
if (feature.nodes?.length) {
|
||||||
sanitized.nodes = sanitized.nodes.concat(feature.nodes)
|
sanitized.nodes = sanitized.nodes.concat(feature.nodes)
|
||||||
feature.nodes.forEach((node) => {
|
|
||||||
if (node?.populationPromises?.length) {
|
|
||||||
sanitized.populationPromises.set(node.type, node.populationPromises)
|
|
||||||
}
|
|
||||||
if (node?.validations?.length) {
|
|
||||||
sanitized.validations.set(node.type, node.validations)
|
|
||||||
}
|
|
||||||
if (node?.converters?.html) {
|
|
||||||
sanitized.converters.html.push(node.converters.html)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if (feature.plugins?.length) {
|
if (feature.plugins?.length) {
|
||||||
feature.plugins.forEach((plugin, i) => {
|
feature.plugins.forEach((plugin, i) => {
|
||||||
@@ -72,11 +52,6 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (feature.markdownTransformers?.length) {
|
|
||||||
sanitized.markdownTransformers = sanitized.markdownTransformers.concat(
|
|
||||||
feature.markdownTransformers,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (feature.floatingSelectToolbar?.sections?.length) {
|
if (feature.floatingSelectToolbar?.sections?.length) {
|
||||||
for (const section of feature.floatingSelectToolbar.sections) {
|
for (const section of feature.floatingSelectToolbar.sections) {
|
||||||
@@ -169,14 +144,13 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
|||||||
return sanitized
|
return sanitized
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeEditorConfig(editorConfig: EditorConfig): SanitizedEditorConfig {
|
export function sanitizeClientEditorConfig(
|
||||||
const resolvedFeatureMap = loadFeatures({
|
lexical: LexicalEditorConfig,
|
||||||
unSanitizedEditorConfig: editorConfig,
|
resolvedClientFeatureMap: ResolvedClientFeatureMap,
|
||||||
})
|
): SanitizedClientEditorConfig {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
features: sanitizeFeatures(resolvedFeatureMap),
|
features: sanitizeClientFeatures(resolvedClientFeatureMap),
|
||||||
lexical: editorConfig.lexical,
|
lexical,
|
||||||
resolvedFeatureMap: resolvedFeatureMap,
|
resolvedFeatureMap: resolvedClientFeatureMap,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import type { FeatureProvider } from '../../features/types'
|
|
||||||
import type { EditorConfig, SanitizedEditorConfig } from './types'
|
|
||||||
|
|
||||||
import { BlockQuoteFeature } from '../../features/BlockQuote'
|
|
||||||
import { HeadingFeature } from '../../features/Heading'
|
|
||||||
import { LinkFeature } from '../../features/Link'
|
|
||||||
import { ParagraphFeature } from '../../features/Paragraph'
|
|
||||||
import { RelationshipFeature } from '../../features/Relationship'
|
|
||||||
import { UploadFeature } from '../../features/Upload'
|
|
||||||
import { AlignFeature } from '../../features/align'
|
|
||||||
import { BoldTextFeature } from '../../features/format/Bold'
|
|
||||||
import { InlineCodeTextFeature } from '../../features/format/InlineCode'
|
|
||||||
import { ItalicTextFeature } from '../../features/format/Italic'
|
|
||||||
import { StrikethroughTextFeature } from '../../features/format/strikethrough'
|
|
||||||
import { SubscriptTextFeature } from '../../features/format/subscript'
|
|
||||||
import { SuperscriptTextFeature } from '../../features/format/superscript'
|
|
||||||
import { UnderlineTextFeature } from '../../features/format/underline'
|
|
||||||
import { IndentFeature } from '../../features/indent'
|
|
||||||
import { CheckListFeature } from '../../features/lists/CheckList'
|
|
||||||
import { OrderedListFeature } from '../../features/lists/OrderedList'
|
|
||||||
import { UnorderedListFeature } from '../../features/lists/UnorderedList'
|
|
||||||
import { sanitizeEditorConfig } from './sanitize'
|
|
||||||
|
|
||||||
export const defaultEditorFeatures: FeatureProvider[] = [
|
|
||||||
BoldTextFeature(),
|
|
||||||
ItalicTextFeature(),
|
|
||||||
UnderlineTextFeature(),
|
|
||||||
StrikethroughTextFeature(),
|
|
||||||
SubscriptTextFeature(),
|
|
||||||
SuperscriptTextFeature(),
|
|
||||||
InlineCodeTextFeature(),
|
|
||||||
ParagraphFeature(),
|
|
||||||
HeadingFeature({}),
|
|
||||||
AlignFeature(),
|
|
||||||
IndentFeature(),
|
|
||||||
UnorderedListFeature(),
|
|
||||||
OrderedListFeature(),
|
|
||||||
CheckListFeature(),
|
|
||||||
LinkFeature({}),
|
|
||||||
RelationshipFeature(),
|
|
||||||
BlockQuoteFeature(),
|
|
||||||
UploadFeature(),
|
|
||||||
//BlocksFeature(), // Adding this by default makes no sense if no blocks are defined
|
|
||||||
]
|
|
||||||
|
|
||||||
export const defaultEditorConfig: EditorConfig = {
|
|
||||||
features: defaultEditorFeatures,
|
|
||||||
lexical: () =>
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
import('./defaultClient').then((module) => {
|
|
||||||
const defaultEditorLexicalConfig = module.defaultEditorLexicalConfig
|
|
||||||
return defaultEditorLexicalConfig
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultSanitizedEditorConfig: SanitizedEditorConfig =
|
|
||||||
sanitizeEditorConfig(defaultEditorConfig)
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||||
|
|
||||||
|
import type { FeatureProviderServer } from '../../../features/types'
|
||||||
|
import type { SanitizedServerEditorConfig, ServerEditorConfig } from '../types'
|
||||||
|
|
||||||
|
import { BlockQuoteFeature } from '../../../features/BlockQuote'
|
||||||
|
import { HeadingFeature } from '../../../features/Heading'
|
||||||
|
import { ParagraphFeature } from '../../../features/Paragraph'
|
||||||
|
import { RelationshipFeature } from '../../../features/Relationship'
|
||||||
|
import { UploadFeature } from '../../../features/Upload'
|
||||||
|
import { AlignFeature } from '../../../features/align/feature.server'
|
||||||
|
import { BoldTextFeature } from '../../../features/format/Bold'
|
||||||
|
import { InlineCodeTextFeature } from '../../../features/format/InlineCode'
|
||||||
|
import { ItalicTextFeature } from '../../../features/format/Italic'
|
||||||
|
import { StrikethroughTextFeature } from '../../../features/format/strikethrough'
|
||||||
|
import { SubscriptTextFeature } from '../../../features/format/subscript'
|
||||||
|
import { SuperscriptTextFeature } from '../../../features/format/superscript'
|
||||||
|
import { UnderlineTextFeature } from '../../../features/format/underline'
|
||||||
|
import { IndentFeature } from '../../../features/indent'
|
||||||
|
import { LinkFeature } from '../../../features/link/feature.server'
|
||||||
|
import { CheckListFeature } from '../../../features/lists/CheckList'
|
||||||
|
import { OrderedListFeature } from '../../../features/lists/OrderedList'
|
||||||
|
import { UnorderedListFeature } from '../../../features/lists/UnorderedList'
|
||||||
|
import { LexicalEditorTheme } from '../../theme/EditorTheme'
|
||||||
|
import { sanitizeServerEditorConfig } from './sanitize'
|
||||||
|
|
||||||
|
export const defaultEditorLexicalConfig: LexicalEditorConfig = {
|
||||||
|
namespace: 'lexical',
|
||||||
|
theme: LexicalEditorTheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultEditorFeatures: FeatureProviderServer<unknown, unknown>[] = [
|
||||||
|
BoldTextFeature(),
|
||||||
|
ItalicTextFeature(),
|
||||||
|
UnderlineTextFeature(),
|
||||||
|
StrikethroughTextFeature(),
|
||||||
|
SubscriptTextFeature(),
|
||||||
|
SuperscriptTextFeature(),
|
||||||
|
InlineCodeTextFeature(),
|
||||||
|
ParagraphFeature(),
|
||||||
|
HeadingFeature({}),
|
||||||
|
AlignFeature(),
|
||||||
|
IndentFeature(),
|
||||||
|
UnorderedListFeature(),
|
||||||
|
OrderedListFeature(),
|
||||||
|
CheckListFeature(),
|
||||||
|
LinkFeature({}),
|
||||||
|
RelationshipFeature(),
|
||||||
|
BlockQuoteFeature(),
|
||||||
|
UploadFeature(),
|
||||||
|
]
|
||||||
|
|
||||||
|
export const defaultEditorConfig: ServerEditorConfig = {
|
||||||
|
features: defaultEditorFeatures,
|
||||||
|
lexical: defaultEditorLexicalConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultSanitizedServerEditorConfig: SanitizedServerEditorConfig =
|
||||||
|
sanitizeServerEditorConfig(defaultEditorConfig)
|
||||||
@@ -1,16 +1,22 @@
|
|||||||
import type { FeatureProvider, FeatureProviderMap, ResolvedFeatureMap } from '../../features/types'
|
import type {
|
||||||
import type { EditorConfig } from './types'
|
FeatureProviderServer,
|
||||||
|
ResolvedServerFeatureMap,
|
||||||
|
ServerFeatureProviderMap,
|
||||||
|
} from '../../../features/types'
|
||||||
|
import type { ServerEditorConfig } from '../types'
|
||||||
|
|
||||||
type DependencyGraph = {
|
type DependencyGraph = {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
dependencies: string[]
|
dependencies: string[]
|
||||||
dependenciesPriority: string[]
|
dependenciesPriority: string[]
|
||||||
dependenciesSoft: string[]
|
dependenciesSoft: string[]
|
||||||
featureProvider: FeatureProvider
|
featureProvider: FeatureProviderServer<unknown, unknown>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDependencyGraph(featureProviders: FeatureProvider[]): DependencyGraph {
|
function createDependencyGraph(
|
||||||
|
featureProviders: FeatureProviderServer<unknown, unknown>[],
|
||||||
|
): DependencyGraph {
|
||||||
const graph: DependencyGraph = {}
|
const graph: DependencyGraph = {}
|
||||||
for (const fp of featureProviders) {
|
for (const fp of featureProviders) {
|
||||||
graph[fp.key] = {
|
graph[fp.key] = {
|
||||||
@@ -23,10 +29,12 @@ function createDependencyGraph(featureProviders: FeatureProvider[]): DependencyG
|
|||||||
return graph
|
return graph
|
||||||
}
|
}
|
||||||
|
|
||||||
function topologicallySortFeatures(featureProviders: FeatureProvider[]): FeatureProvider[] {
|
function topologicallySortFeatures(
|
||||||
|
featureProviders: FeatureProviderServer<unknown, unknown>[],
|
||||||
|
): FeatureProviderServer<unknown, unknown>[] {
|
||||||
const graph = createDependencyGraph(featureProviders)
|
const graph = createDependencyGraph(featureProviders)
|
||||||
const visited: { [key: string]: boolean } = {}
|
const visited: { [key: string]: boolean } = {}
|
||||||
const stack: FeatureProvider[] = []
|
const stack: FeatureProviderServer<unknown, unknown>[] = []
|
||||||
|
|
||||||
for (const key in graph) {
|
for (const key in graph) {
|
||||||
if (!visited[key]) {
|
if (!visited[key]) {
|
||||||
@@ -41,7 +49,7 @@ function visit(
|
|||||||
graph: DependencyGraph,
|
graph: DependencyGraph,
|
||||||
key: string,
|
key: string,
|
||||||
visited: { [key: string]: boolean },
|
visited: { [key: string]: boolean },
|
||||||
stack: FeatureProvider[],
|
stack: FeatureProviderServer<unknown, unknown>[],
|
||||||
currentPath: string[] = [],
|
currentPath: string[] = [],
|
||||||
) {
|
) {
|
||||||
if (!graph[key]) {
|
if (!graph[key]) {
|
||||||
@@ -90,16 +98,16 @@ function visit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sortFeaturesForOptimalLoading(
|
export function sortFeaturesForOptimalLoading(
|
||||||
featureProviders: FeatureProvider[],
|
featureProviders: FeatureProviderServer<unknown, unknown>[],
|
||||||
): FeatureProvider[] {
|
): FeatureProviderServer<unknown, unknown>[] {
|
||||||
return topologicallySortFeatures(featureProviders)
|
return topologicallySortFeatures(featureProviders)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadFeatures({
|
export function loadFeatures({
|
||||||
unSanitizedEditorConfig,
|
unSanitizedEditorConfig,
|
||||||
}: {
|
}: {
|
||||||
unSanitizedEditorConfig: EditorConfig
|
unSanitizedEditorConfig: ServerEditorConfig
|
||||||
}): ResolvedFeatureMap {
|
}): ResolvedServerFeatureMap {
|
||||||
// First remove all duplicate features. The LAST feature with a given key wins.
|
// First remove all duplicate features. The LAST feature with a given key wins.
|
||||||
unSanitizedEditorConfig.features = unSanitizedEditorConfig.features
|
unSanitizedEditorConfig.features = unSanitizedEditorConfig.features
|
||||||
.reverse()
|
.reverse()
|
||||||
@@ -109,15 +117,18 @@ export function loadFeatures({
|
|||||||
})
|
})
|
||||||
.reverse()
|
.reverse()
|
||||||
|
|
||||||
const featureProviderMap: FeatureProviderMap = new Map(
|
|
||||||
unSanitizedEditorConfig.features.map((f) => [f.key, f] as [string, FeatureProvider]),
|
|
||||||
)
|
|
||||||
|
|
||||||
unSanitizedEditorConfig.features = sortFeaturesForOptimalLoading(unSanitizedEditorConfig.features)
|
unSanitizedEditorConfig.features = sortFeaturesForOptimalLoading(unSanitizedEditorConfig.features)
|
||||||
|
|
||||||
const resolvedFeatures: ResolvedFeatureMap = new Map()
|
const featureProviderMap: ServerFeatureProviderMap = new Map(
|
||||||
|
unSanitizedEditorConfig.features.map(
|
||||||
|
(f) => [f.key, f] as [string, FeatureProviderServer<unknown, unknown>],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolvedFeatures: ResolvedServerFeatureMap = new Map()
|
||||||
|
|
||||||
// Make sure all dependencies declared in the respective features exist
|
// Make sure all dependencies declared in the respective features exist
|
||||||
|
let loaded = 0
|
||||||
for (const featureProvider of unSanitizedEditorConfig.features) {
|
for (const featureProvider of unSanitizedEditorConfig.features) {
|
||||||
if (!featureProvider.key) {
|
if (!featureProvider.key) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -163,12 +174,14 @@ export function loadFeatures({
|
|||||||
})
|
})
|
||||||
resolvedFeatures.set(featureProvider.key, {
|
resolvedFeatures.set(featureProvider.key, {
|
||||||
...feature,
|
...feature,
|
||||||
Component: featureProvider.Component,
|
|
||||||
dependencies: featureProvider.dependencies,
|
dependencies: featureProvider.dependencies,
|
||||||
dependenciesPriority: featureProvider.dependenciesPriority,
|
dependenciesPriority: featureProvider.dependenciesPriority,
|
||||||
dependenciesSoft: featureProvider.dependenciesSoft,
|
dependenciesSoft: featureProvider.dependenciesSoft,
|
||||||
key: featureProvider.key,
|
key: featureProvider.key,
|
||||||
|
order: loaded,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
loaded++
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolvedFeatures
|
return resolvedFeatures
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import type { ResolvedServerFeatureMap, SanitizedServerFeatures } from '../../../features/types'
|
||||||
|
import type { SanitizedServerEditorConfig, ServerEditorConfig } from '../types'
|
||||||
|
|
||||||
|
import { loadFeatures } from './loader'
|
||||||
|
|
||||||
|
export const sanitizeServerFeatures = (
|
||||||
|
features: ResolvedServerFeatureMap,
|
||||||
|
): SanitizedServerFeatures => {
|
||||||
|
const sanitized: SanitizedServerFeatures = {
|
||||||
|
converters: {
|
||||||
|
html: [],
|
||||||
|
},
|
||||||
|
enabledFeatures: [],
|
||||||
|
|
||||||
|
generatedTypes: {
|
||||||
|
modifyOutputSchemas: [],
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
afterReadPromises: [],
|
||||||
|
},
|
||||||
|
markdownTransformers: [],
|
||||||
|
nodes: [],
|
||||||
|
populationPromises: new Map(),
|
||||||
|
|
||||||
|
validations: new Map(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!features?.size) {
|
||||||
|
return sanitized
|
||||||
|
}
|
||||||
|
|
||||||
|
features.forEach((feature) => {
|
||||||
|
if (feature?.generatedTypes?.modifyOutputSchema) {
|
||||||
|
sanitized.generatedTypes.modifyOutputSchemas.push(feature.generatedTypes.modifyOutputSchema)
|
||||||
|
}
|
||||||
|
if (feature.hooks) {
|
||||||
|
if (feature.hooks.afterReadPromise) {
|
||||||
|
sanitized.hooks.afterReadPromises = sanitized.hooks.afterReadPromises.concat(
|
||||||
|
feature.hooks.afterReadPromise,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feature.nodes?.length) {
|
||||||
|
sanitized.nodes = sanitized.nodes.concat(feature.nodes)
|
||||||
|
feature.nodes.forEach((node) => {
|
||||||
|
const nodeType = 'with' in node.node ? node.node.replace.getType() : node.node.getType() // TODO: Idk if this works for node replacements
|
||||||
|
if (node?.populationPromises?.length) {
|
||||||
|
sanitized.populationPromises.set(nodeType, node.populationPromises)
|
||||||
|
}
|
||||||
|
if (node?.validations?.length) {
|
||||||
|
sanitized.validations.set(nodeType, node.validations)
|
||||||
|
}
|
||||||
|
if (node?.converters?.html) {
|
||||||
|
sanitized.converters.html.push(node.converters.html)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feature.markdownTransformers?.length) {
|
||||||
|
sanitized.markdownTransformers = sanitized.markdownTransformers.concat(
|
||||||
|
feature.markdownTransformers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitized.enabledFeatures.push(feature.key)
|
||||||
|
})
|
||||||
|
|
||||||
|
return sanitized
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeServerEditorConfig(
|
||||||
|
editorConfig: ServerEditorConfig,
|
||||||
|
): SanitizedServerEditorConfig {
|
||||||
|
const resolvedFeatureMap = loadFeatures({
|
||||||
|
unSanitizedEditorConfig: editorConfig,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
features: sanitizeServerFeatures(resolvedFeatureMap),
|
||||||
|
lexical: editorConfig.lexical,
|
||||||
|
resolvedFeatureMap: resolvedFeatureMap,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,32 @@
|
|||||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||||
|
|
||||||
import type { FeatureProvider, ResolvedFeatureMap, SanitizedFeatures } from '../../features/types'
|
import type {
|
||||||
|
FeatureProviderClient,
|
||||||
|
FeatureProviderServer,
|
||||||
|
ResolvedClientFeatureMap,
|
||||||
|
ResolvedServerFeatureMap,
|
||||||
|
SanitizedClientFeatures,
|
||||||
|
SanitizedServerFeatures,
|
||||||
|
} from '../../features/types'
|
||||||
|
|
||||||
export type EditorConfig = {
|
export type ServerEditorConfig = {
|
||||||
features: FeatureProvider[]
|
features: FeatureProviderServer<unknown, unknown>[]
|
||||||
lexical?: () => Promise<LexicalEditorConfig>
|
lexical?: LexicalEditorConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SanitizedEditorConfig = {
|
export type SanitizedServerEditorConfig = {
|
||||||
features: SanitizedFeatures
|
features: SanitizedServerFeatures
|
||||||
lexical: () => Promise<LexicalEditorConfig>
|
lexical: LexicalEditorConfig
|
||||||
resolvedFeatureMap: ResolvedFeatureMap
|
resolvedFeatureMap: ResolvedServerFeatureMap
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientEditorConfig = {
|
||||||
|
features: FeatureProviderClient<unknown>[]
|
||||||
|
lexical?: LexicalEditorConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SanitizedClientEditorConfig = {
|
||||||
|
features: SanitizedClientFeatures
|
||||||
|
lexical: LexicalEditorConfig
|
||||||
|
resolvedFeatureMap: ResolvedClientFeatureMap
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import type { Klass, LexicalNode } from 'lexical'
|
import type { Klass, LexicalNode } from 'lexical'
|
||||||
import type { LexicalNodeReplacement } from 'lexical'
|
import type { LexicalNodeReplacement } from 'lexical'
|
||||||
|
|
||||||
import type { SanitizedEditorConfig } from '../config/types'
|
import type { SanitizedClientEditorConfig, SanitizedServerEditorConfig } from '../config/types'
|
||||||
|
|
||||||
export function getEnabledNodes({
|
export function getEnabledNodes({
|
||||||
editorConfig,
|
editorConfig,
|
||||||
}: {
|
}: {
|
||||||
editorConfig: SanitizedEditorConfig
|
editorConfig: SanitizedClientEditorConfig | SanitizedServerEditorConfig
|
||||||
}): Array<Klass<LexicalNode> | LexicalNodeReplacement> {
|
}): Array<Klass<LexicalNode> | LexicalNodeReplacement> {
|
||||||
return editorConfig.features.nodes.map((node) => node.node)
|
return editorConfig.features.nodes.map((node) => {
|
||||||
|
if ('node' in node) {
|
||||||
|
return node.node
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
|||||||
import { mergeRegister } from '@lexical/utils'
|
import { mergeRegister } from '@lexical/utils'
|
||||||
import { $getSelection } from 'lexical'
|
import { $getSelection } from 'lexical'
|
||||||
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import * as React from 'react'
|
import React from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
import type { FloatingToolbarSectionEntry } from '../types'
|
import type { FloatingToolbarSectionEntry } from '../types'
|
||||||
@@ -24,7 +24,7 @@ export function DropDownItem({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
entry: FloatingToolbarSectionEntry
|
entry: FloatingToolbarSectionEntry
|
||||||
title?: string
|
title?: string
|
||||||
}): JSX.Element {
|
}): React.ReactNode {
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
const [enabled, setEnabled] = useState<boolean>(true)
|
const [enabled, setEnabled] = useState<boolean>(true)
|
||||||
const [active, setActive] = useState<boolean>(false)
|
const [active, setActive] = useState<boolean>(false)
|
||||||
@@ -209,7 +209,7 @@ export function DropDown({
|
|||||||
children: ReactNode
|
children: ReactNode
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
stopCloseOnClickSelf?: boolean
|
stopCloseOnClickSelf?: boolean
|
||||||
}): JSX.Element {
|
}): React.ReactNode {
|
||||||
const dropDownRef = useRef<HTMLDivElement>(null)
|
const dropDownRef = useRef<HTMLDivElement>(null)
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||||
const [showDropDown, setShowDropDown] = useState(false)
|
const [showDropDown, setShowDropDown] = useState(false)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '@payloadcms/ui/scss';
|
@import '../../../../../../../ui/src/scss/styles.scss';
|
||||||
|
|
||||||
.floating-select-toolbar-popup__dropdown {
|
.floating-select-toolbar-popup__dropdown {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React, { useMemo } from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const baseClass = 'floating-select-toolbar-popup__dropdown'
|
const baseClass = 'floating-select-toolbar-popup__dropdown'
|
||||||
|
|
||||||
@@ -19,43 +19,17 @@ export const ToolbarEntry = ({
|
|||||||
editor: LexicalEditor
|
editor: LexicalEditor
|
||||||
entry: FloatingToolbarSectionEntry
|
entry: FloatingToolbarSectionEntry
|
||||||
}) => {
|
}) => {
|
||||||
const Component = useMemo(() => {
|
|
||||||
return entry?.Component
|
|
||||||
? React.lazy(() =>
|
|
||||||
entry.Component().then((resolvedComponent) => ({
|
|
||||||
default: resolvedComponent,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
}, [entry])
|
|
||||||
|
|
||||||
const ChildComponent = useMemo(() => {
|
|
||||||
return entry?.ChildComponent
|
|
||||||
? React.lazy(() =>
|
|
||||||
entry.ChildComponent().then((resolvedChildComponent) => ({
|
|
||||||
default: resolvedChildComponent,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
}, [entry])
|
|
||||||
|
|
||||||
if (entry.Component) {
|
if (entry.Component) {
|
||||||
return (
|
return (
|
||||||
Component && (
|
entry?.Component && (
|
||||||
<React.Suspense>
|
<entry.Component anchorElem={anchorElem} editor={editor} entry={entry} key={entry.key} />
|
||||||
<Component anchorElem={anchorElem} editor={editor} entry={entry} key={entry.key} />
|
|
||||||
</React.Suspense>
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropDownItem entry={entry} key={entry.key}>
|
<DropDownItem entry={entry} key={entry.key}>
|
||||||
{ChildComponent && (
|
{entry?.ChildComponent && <entry.ChildComponent />}
|
||||||
<React.Suspense>
|
|
||||||
<ChildComponent />
|
|
||||||
</React.Suspense>
|
|
||||||
)}
|
|
||||||
<span className="text">{entry.label}</span>
|
<span className="text">{entry.label}</span>
|
||||||
</DropDownItem>
|
</DropDownItem>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '@payloadcms/ui/scss';
|
@import '../../../../../../ui/src/scss/styles.scss';
|
||||||
|
|
||||||
html[data-theme='light'] {
|
html[data-theme='light'] {
|
||||||
.floating-select-toolbar-popup {
|
.floating-select-toolbar-popup {
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import {
|
|||||||
COMMAND_PRIORITY_LOW,
|
COMMAND_PRIORITY_LOW,
|
||||||
SELECTION_CHANGE_COMMAND,
|
SELECTION_CHANGE_COMMAND,
|
||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
import type { FloatingToolbarSection, FloatingToolbarSectionEntry } from './types'
|
import type { FloatingToolbarSection, FloatingToolbarSectionEntry } from './types'
|
||||||
|
|
||||||
import { useEditorConfigContext } from '../../config/EditorConfigProvider'
|
import { useEditorConfigContext } from '../../config/client/EditorConfigProvider'
|
||||||
import { getDOMRangeRect } from '../../utils/getDOMRangeRect'
|
import { getDOMRangeRect } from '../../utils/getDOMRangeRect'
|
||||||
import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition'
|
import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition'
|
||||||
import { ToolbarButton } from './ToolbarButton'
|
import { ToolbarButton } from './ToolbarButton'
|
||||||
@@ -31,44 +31,18 @@ function ButtonSectionEntry({
|
|||||||
anchorElem: HTMLElement
|
anchorElem: HTMLElement
|
||||||
editor: LexicalEditor
|
editor: LexicalEditor
|
||||||
entry: FloatingToolbarSectionEntry
|
entry: FloatingToolbarSectionEntry
|
||||||
}): JSX.Element {
|
}): React.ReactNode {
|
||||||
const Component = useMemo(() => {
|
|
||||||
return entry?.Component
|
|
||||||
? React.lazy(() =>
|
|
||||||
entry.Component().then((resolvedComponent) => ({
|
|
||||||
default: resolvedComponent,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
}, [entry])
|
|
||||||
|
|
||||||
const ChildComponent = useMemo(() => {
|
|
||||||
return entry?.ChildComponent
|
|
||||||
? React.lazy(() =>
|
|
||||||
entry.ChildComponent().then((resolvedChildComponent) => ({
|
|
||||||
default: resolvedChildComponent,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
}, [entry])
|
|
||||||
|
|
||||||
if (entry.Component) {
|
if (entry.Component) {
|
||||||
return (
|
return (
|
||||||
Component && (
|
entry?.Component && (
|
||||||
<React.Suspense>
|
<entry.Component anchorElem={anchorElem} editor={editor} entry={entry} key={entry.key} />
|
||||||
<Component anchorElem={anchorElem} editor={editor} entry={entry} key={entry.key} />{' '}
|
|
||||||
</React.Suspense>
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolbarButton entry={entry} key={entry.key}>
|
<ToolbarButton entry={entry} key={entry.key}>
|
||||||
{ChildComponent && (
|
{entry?.ChildComponent && <entry.ChildComponent />}
|
||||||
<React.Suspense>
|
|
||||||
<ChildComponent />
|
|
||||||
</React.Suspense>
|
|
||||||
)}
|
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -83,18 +57,13 @@ function ToolbarSection({
|
|||||||
editor: LexicalEditor
|
editor: LexicalEditor
|
||||||
index: number
|
index: number
|
||||||
section: FloatingToolbarSection
|
section: FloatingToolbarSection
|
||||||
}): JSX.Element {
|
}): React.ReactNode {
|
||||||
const { editorConfig } = useEditorConfigContext()
|
const { editorConfig } = useEditorConfigContext()
|
||||||
|
|
||||||
const Icon = useMemo(() => {
|
const Icon =
|
||||||
return section?.type === 'dropdown' && section.entries.length && section.ChildComponent
|
section?.type === 'dropdown' && section.entries.length && section.ChildComponent
|
||||||
? React.lazy(() =>
|
? section.ChildComponent
|
||||||
section.ChildComponent().then((resolvedComponent) => ({
|
|
||||||
default: resolvedComponent,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
: null
|
: null
|
||||||
}, [section])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -104,7 +73,6 @@ function ToolbarSection({
|
|||||||
{section.type === 'dropdown' &&
|
{section.type === 'dropdown' &&
|
||||||
section.entries.length &&
|
section.entries.length &&
|
||||||
(Icon ? (
|
(Icon ? (
|
||||||
<React.Suspense>
|
|
||||||
<ToolbarDropdown
|
<ToolbarDropdown
|
||||||
Icon={Icon}
|
Icon={Icon}
|
||||||
anchorElem={anchorElem}
|
anchorElem={anchorElem}
|
||||||
@@ -112,7 +80,6 @@ function ToolbarSection({
|
|||||||
entries={section.entries}
|
entries={section.entries}
|
||||||
sectionKey={section.key}
|
sectionKey={section.key}
|
||||||
/>
|
/>
|
||||||
</React.Suspense>
|
|
||||||
) : (
|
) : (
|
||||||
<ToolbarDropdown
|
<ToolbarDropdown
|
||||||
anchorElem={anchorElem}
|
anchorElem={anchorElem}
|
||||||
@@ -146,7 +113,7 @@ function FloatingSelectToolbar({
|
|||||||
}: {
|
}: {
|
||||||
anchorElem: HTMLElement
|
anchorElem: HTMLElement
|
||||||
editor: LexicalEditor
|
editor: LexicalEditor
|
||||||
}): JSX.Element {
|
}): React.ReactNode {
|
||||||
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null)
|
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null)
|
||||||
const caretRef = useRef<HTMLDivElement | null>(null)
|
const caretRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type React from 'react'
|
|||||||
|
|
||||||
export type FloatingToolbarSection =
|
export type FloatingToolbarSection =
|
||||||
| {
|
| {
|
||||||
ChildComponent?: () => Promise<React.FC>
|
ChildComponent?: React.FC
|
||||||
entries: Array<FloatingToolbarSectionEntry>
|
entries: Array<FloatingToolbarSectionEntry>
|
||||||
key: string
|
key: string
|
||||||
order?: number
|
order?: number
|
||||||
@@ -17,15 +17,13 @@ export type FloatingToolbarSection =
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type FloatingToolbarSectionEntry = {
|
export type FloatingToolbarSectionEntry = {
|
||||||
ChildComponent?: () => Promise<React.FC>
|
ChildComponent?: React.FC
|
||||||
/** Use component to ignore the children and onClick properties. It does not use the default, pre-defined format Button component */
|
/** Use component to ignore the children and onClick properties. It does not use the default, pre-defined format Button component */
|
||||||
Component?: () => Promise<
|
Component?: React.FC<{
|
||||||
React.FC<{
|
|
||||||
anchorElem: HTMLElement
|
anchorElem: HTMLElement
|
||||||
editor: LexicalEditor
|
editor: LexicalEditor
|
||||||
entry: FloatingToolbarSectionEntry
|
entry: FloatingToolbarSectionEntry
|
||||||
}>
|
}>
|
||||||
>
|
|
||||||
isActive?: ({ editor, selection }: { editor: LexicalEditor; selection: BaseSelection }) => boolean
|
isActive?: ({ editor, selection }: { editor: LexicalEditor; selection: BaseSelection }) => boolean
|
||||||
isEnabled?: ({
|
isEnabled?: ({
|
||||||
editor,
|
editor,
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
import { MarkdownShortcutPlugin as LexicalMarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin'
|
import { MarkdownShortcutPlugin as LexicalMarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { useEditorConfigContext } from '../../config/EditorConfigProvider'
|
import { useEditorConfigContext } from '../../config/client/EditorConfigProvider'
|
||||||
|
|
||||||
export const MarkdownShortcutPlugin: React.FC = () => {
|
export const MarkdownShortcutPlugin: React.FC = () => {
|
||||||
const { editorConfig } = useEditorConfigContext()
|
const { editorConfig } = useEditorConfigContext()
|
||||||
|
|
||||||
|
console.log('traaaaa', editorConfig.features.markdownTransformers)
|
||||||
return <LexicalMarkdownShortcutPlugin transformers={editorConfig.features.markdownTransformers} />
|
return <LexicalMarkdownShortcutPlugin transformers={editorConfig.features.markdownTransformers} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type React from 'react'
|
|||||||
|
|
||||||
export class SlashMenuOption {
|
export class SlashMenuOption {
|
||||||
// Icon for display
|
// Icon for display
|
||||||
Icon: () => Promise<React.FC>
|
Icon: React.FC
|
||||||
|
|
||||||
displayName?: (({ i18n }: { i18n: I18n }) => string) | string
|
displayName?: (({ i18n }: { i18n: I18n }) => string) | string
|
||||||
// Used for class names and, if displayName is not provided, for display.
|
// Used for class names and, if displayName is not provided, for display.
|
||||||
@@ -22,7 +22,7 @@ export class SlashMenuOption {
|
|||||||
constructor(
|
constructor(
|
||||||
key: string,
|
key: string,
|
||||||
options: {
|
options: {
|
||||||
Icon: () => Promise<React.FC>
|
Icon: React.FC
|
||||||
displayName?: (({ i18n }: { i18n: I18n }) => string) | string
|
displayName?: (({ i18n }: { i18n: I18n }) => string) | string
|
||||||
keyboardShortcut?: string
|
keyboardShortcut?: string
|
||||||
keywords?: Array<string>
|
keywords?: Array<string>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '@payloadcms/ui/scss';
|
@import '../../../../../../ui/src/scss/styles.scss';
|
||||||
|
|
||||||
html[data-theme='light'] {
|
html[data-theme='light'] {
|
||||||
.slash-menu-popup {
|
.slash-menu-popup {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import * as ReactDOM from 'react-dom'
|
|||||||
|
|
||||||
import type { SlashMenuGroup, SlashMenuOption } from './LexicalTypeaheadMenuPlugin/types'
|
import type { SlashMenuGroup, SlashMenuOption } from './LexicalTypeaheadMenuPlugin/types'
|
||||||
|
|
||||||
import { useEditorConfigContext } from '../../config/EditorConfigProvider'
|
import { useEditorConfigContext } from '../../config/client/EditorConfigProvider'
|
||||||
import { LexicalTypeaheadMenuPlugin } from './LexicalTypeaheadMenuPlugin'
|
import { LexicalTypeaheadMenuPlugin } from './LexicalTypeaheadMenuPlugin'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { useMenuTriggerMatch } from './useMenuTriggerMatch'
|
import { useMenuTriggerMatch } from './useMenuTriggerMatch'
|
||||||
@@ -45,16 +45,6 @@ function SlashMenuItem({
|
|||||||
title = title.substring(0, 25) + '...'
|
title = title.substring(0, 25) + '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
const LazyIcon = useMemo(() => {
|
|
||||||
return option?.Icon
|
|
||||||
? React.lazy(() =>
|
|
||||||
option.Icon().then((resolvedIcon) => ({
|
|
||||||
default: resolvedIcon,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
}, [option])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
aria-selected={isSelected}
|
aria-selected={isSelected}
|
||||||
@@ -68,11 +58,7 @@ function SlashMenuItem({
|
|||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{LazyIcon && (
|
{option?.Icon && <option.Icon />}
|
||||||
<React.Suspense>
|
|
||||||
<LazyIcon />
|
|
||||||
</React.Suspense>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className={`${baseClass}__item-text`}>{title}</span>
|
<span className={`${baseClass}__item-text`}>{title}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,23 +1,104 @@
|
|||||||
import type { RichTextAdapter } from 'payload/types'
|
import type { RichTextAdapter } from 'payload/types'
|
||||||
|
|
||||||
|
import { mapFields } from '@payloadcms/ui/utilities'
|
||||||
|
import { sanitizeFields } from 'payload/config'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { ResolvedFeatureMap } from './field/features/types'
|
import type { ResolvedServerFeatureMap } from './field/features/types'
|
||||||
|
import type { GeneratedFeatureProviderComponent } from './types'
|
||||||
|
|
||||||
export const getGenerateComponentMap =
|
export const getGenerateComponentMap =
|
||||||
(args: { resolvedFeatureMap: ResolvedFeatureMap }): RichTextAdapter['generateComponentMap'] =>
|
(args: {
|
||||||
({ config }) => {
|
resolvedFeatureMap: ResolvedServerFeatureMap
|
||||||
|
}): RichTextAdapter['generateComponentMap'] =>
|
||||||
|
({ config, schemaPath }) => {
|
||||||
|
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||||
|
|
||||||
const componentMap = new Map()
|
const componentMap = new Map()
|
||||||
|
|
||||||
console.log('args.resolvedFeatureMap', args.resolvedFeatureMap)
|
// turn args.resolvedFeatureMap into an array of [key, value] pairs, ordered by value.order, lowest order first:
|
||||||
|
const resolvedFeatureMapArray = Array.from(args.resolvedFeatureMap.entries()).sort(
|
||||||
|
(a, b) => a[1].order - b[1].order,
|
||||||
|
)
|
||||||
|
|
||||||
for (const key of args.resolvedFeatureMap.keys()) {
|
componentMap.set(
|
||||||
console.log('key', key)
|
`features`,
|
||||||
const resolvedFeature = args.resolvedFeatureMap.get(key)
|
resolvedFeatureMapArray.map(([featureKey, resolvedFeature]) => {
|
||||||
const Component = resolvedFeature.Component
|
const ClientComponent = resolvedFeature.ClientComponent
|
||||||
componentMap.set(`feature.${key}`, <Component />)
|
const clientComponentProps = resolvedFeature.clientFeatureProps
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Feature Component Maps
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
'generateComponentMap' in resolvedFeature &&
|
||||||
|
typeof resolvedFeature.generateComponentMap === 'function'
|
||||||
|
) {
|
||||||
|
const components = resolvedFeature.generateComponentMap({
|
||||||
|
config,
|
||||||
|
props: resolvedFeature.serverFeatureProps,
|
||||||
|
schemaPath,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const componentKey in components) {
|
||||||
|
const Component = components[componentKey]
|
||||||
|
if (Component) {
|
||||||
|
componentMap.set(`feature.${featureKey}.components.${componentKey}`, <Component />)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('componentMaaap', componentMap)
|
/**
|
||||||
|
* Handle Feature Schema Maps (rendered fields)
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
'generateSchemaMap' in resolvedFeature &&
|
||||||
|
typeof resolvedFeature.generateSchemaMap === 'function'
|
||||||
|
) {
|
||||||
|
const schemas = resolvedFeature.generateSchemaMap({
|
||||||
|
config,
|
||||||
|
props: resolvedFeature.serverFeatureProps,
|
||||||
|
schemaMap: new Map(),
|
||||||
|
schemaPath,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const schemaKey in schemas) {
|
||||||
|
const fields = schemas[schemaKey]
|
||||||
|
|
||||||
|
const sanitizedFields = sanitizeFields({
|
||||||
|
config,
|
||||||
|
fields,
|
||||||
|
validRelationships,
|
||||||
|
})
|
||||||
|
|
||||||
|
const mappedFields = mapFields({
|
||||||
|
config,
|
||||||
|
fieldSchema: sanitizedFields,
|
||||||
|
operation: 'update',
|
||||||
|
permissions: {},
|
||||||
|
readOnly: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
componentMap.set(`feature.${featureKey}.fields.${schemaKey}`, mappedFields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ClientComponent:
|
||||||
|
clientComponentProps && typeof clientComponentProps === 'object' ? (
|
||||||
|
<ClientComponent
|
||||||
|
{...clientComponentProps}
|
||||||
|
featureKey={resolvedFeature.key}
|
||||||
|
order={resolvedFeature.order}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ClientComponent featureKey={resolvedFeature.key} order={resolvedFeature.order} />
|
||||||
|
),
|
||||||
|
key: resolvedFeature.key,
|
||||||
|
order: resolvedFeature.order,
|
||||||
|
} as GeneratedFeatureProviderComponent
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
return componentMap
|
return componentMap
|
||||||
}
|
}
|
||||||
|
|||||||
40
packages/richtext-lexical/src/generateSchemaMap.ts
Normal file
40
packages/richtext-lexical/src/generateSchemaMap.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { RichTextAdapter } from 'payload/types'
|
||||||
|
|
||||||
|
import { sanitizeFields } from 'payload/config'
|
||||||
|
|
||||||
|
import type { ResolvedServerFeatureMap } from './field/features/types'
|
||||||
|
|
||||||
|
export const getGenerateSchemaMap =
|
||||||
|
(args: { resolvedFeatureMap: ResolvedServerFeatureMap }): RichTextAdapter['generateSchemaMap'] =>
|
||||||
|
({ config, schemaMap, schemaPath }) => {
|
||||||
|
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||||
|
|
||||||
|
for (const [featureKey, resolvedFeature] of args.resolvedFeatureMap.entries()) {
|
||||||
|
if (
|
||||||
|
!('generateSchemaMap' in resolvedFeature) ||
|
||||||
|
typeof resolvedFeature.generateSchemaMap !== 'function'
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const schemas = resolvedFeature.generateSchemaMap({
|
||||||
|
config,
|
||||||
|
props: resolvedFeature.serverFeatureProps,
|
||||||
|
schemaMap,
|
||||||
|
schemaPath,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const schemaKey in schemas) {
|
||||||
|
const fields = schemas[schemaKey]
|
||||||
|
|
||||||
|
const sanitizedFields = sanitizeFields({
|
||||||
|
config,
|
||||||
|
fields,
|
||||||
|
validRelationships,
|
||||||
|
})
|
||||||
|
|
||||||
|
schemaMap.set(`${schemaPath}.feature.${featureKey}.${schemaKey}`, sanitizedFields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return schemaMap
|
||||||
|
}
|
||||||
@@ -5,45 +5,51 @@ import type { RichTextAdapter } from 'payload/types'
|
|||||||
|
|
||||||
import { withNullableJSONSchemaType } from 'payload/utilities'
|
import { withNullableJSONSchemaType } from 'payload/utilities'
|
||||||
|
|
||||||
import type { FeatureProvider, ResolvedFeatureMap } from './field/features/types'
|
import type { FeatureProviderServer, ResolvedServerFeatureMap } from './field/features/types'
|
||||||
import type { EditorConfig, SanitizedEditorConfig } from './field/lexical/config/types'
|
import type { SanitizedServerEditorConfig } from './field/lexical/config/types'
|
||||||
import type { AdapterProps } from './types'
|
import type { AdapterProps } from './types'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
defaultEditorConfig,
|
defaultEditorConfig,
|
||||||
defaultEditorFeatures,
|
defaultEditorFeatures,
|
||||||
defaultSanitizedEditorConfig,
|
defaultSanitizedServerEditorConfig,
|
||||||
} from './field/lexical/config/default'
|
} from './field/lexical/config/server/default'
|
||||||
import { loadFeatures } from './field/lexical/config/loader'
|
import { loadFeatures } from './field/lexical/config/server/loader'
|
||||||
import { sanitizeFeatures } from './field/lexical/config/sanitize'
|
import { sanitizeServerFeatures } from './field/lexical/config/server/sanitize'
|
||||||
import { cloneDeep } from './field/lexical/utils/cloneDeep'
|
import { cloneDeep } from './field/lexical/utils/cloneDeep'
|
||||||
import { getGenerateComponentMap } from './generateComponentMap'
|
import { getGenerateComponentMap } from './generateComponentMap'
|
||||||
|
import { getGenerateSchemaMap } from './generateSchemaMap'
|
||||||
import { richTextRelationshipPromise } from './populate/richTextRelationshipPromise'
|
import { richTextRelationshipPromise } from './populate/richTextRelationshipPromise'
|
||||||
import { richTextValidateHOC } from './validate'
|
import { richTextValidateHOC } from './validate'
|
||||||
|
|
||||||
export type LexicalEditorProps = {
|
export type LexicalEditorProps = {
|
||||||
features?:
|
features?:
|
||||||
| (({ defaultFeatures }: { defaultFeatures: FeatureProvider[] }) => FeatureProvider[])
|
| (({
|
||||||
| FeatureProvider[]
|
defaultFeatures,
|
||||||
|
}: {
|
||||||
|
defaultFeatures: FeatureProviderServer<unknown, unknown>[]
|
||||||
|
}) => FeatureProviderServer<unknown, unknown>[])
|
||||||
|
| FeatureProviderServer<unknown, unknown>[]
|
||||||
lexical?: LexicalEditorConfig
|
lexical?: LexicalEditorConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||||
export type LexicalRichTextAdapter = RichTextAdapter<SerializedEditorState, AdapterProps, any> & {
|
export type LexicalRichTextAdapter = RichTextAdapter<SerializedEditorState, AdapterProps, any> & {
|
||||||
editorConfig: SanitizedEditorConfig
|
editorConfig: SanitizedServerEditorConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapter {
|
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapter {
|
||||||
let resolvedFeatureMap: ResolvedFeatureMap = null // For client and sending to client. Better than serializing completely on client. That way the feature loading can be done on the server.
|
let resolvedFeatureMap: ResolvedServerFeatureMap = null
|
||||||
|
|
||||||
let finalSanitizedEditorConfig: SanitizedEditorConfig // For server only
|
let finalSanitizedEditorConfig: SanitizedServerEditorConfig // For server only
|
||||||
if (!props || (!props.features && !props.lexical)) {
|
if (!props || (!props.features && !props.lexical)) {
|
||||||
finalSanitizedEditorConfig = cloneDeep(defaultSanitizedEditorConfig)
|
finalSanitizedEditorConfig = cloneDeep(defaultSanitizedServerEditorConfig)
|
||||||
resolvedFeatureMap = finalSanitizedEditorConfig.resolvedFeatureMap
|
resolvedFeatureMap = finalSanitizedEditorConfig.resolvedFeatureMap
|
||||||
} else {
|
} else {
|
||||||
let features: FeatureProvider[] =
|
let features: FeatureProviderServer<unknown, unknown>[] =
|
||||||
props.features && typeof props.features === 'function'
|
props.features && typeof props.features === 'function'
|
||||||
? props.features({ defaultFeatures: cloneDeep(defaultEditorFeatures) })
|
? props.features({ defaultFeatures: cloneDeep(defaultEditorFeatures) })
|
||||||
: (props.features as FeatureProvider[])
|
: (props.features as FeatureProviderServer<unknown, unknown>[])
|
||||||
if (!features) {
|
if (!features) {
|
||||||
features = cloneDeep(defaultEditorFeatures)
|
features = cloneDeep(defaultEditorFeatures)
|
||||||
}
|
}
|
||||||
@@ -53,14 +59,14 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
|||||||
resolvedFeatureMap = loadFeatures({
|
resolvedFeatureMap = loadFeatures({
|
||||||
unSanitizedEditorConfig: {
|
unSanitizedEditorConfig: {
|
||||||
features,
|
features,
|
||||||
lexical: lexical ? () => Promise.resolve(lexical) : defaultEditorConfig.lexical,
|
lexical: lexical ? lexical : defaultEditorConfig.lexical,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
finalSanitizedEditorConfig = {
|
finalSanitizedEditorConfig = {
|
||||||
features: sanitizeFeatures(resolvedFeatureMap),
|
features: sanitizeServerFeatures(resolvedFeatureMap),
|
||||||
lexical: lexical ? () => Promise.resolve(lexical) : defaultEditorConfig.lexical,
|
lexical: lexical ? lexical : defaultEditorConfig.lexical,
|
||||||
resolvedFeatureMap: resolvedFeatureMap,
|
resolvedFeatureMap,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +119,10 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
|||||||
},
|
},
|
||||||
editorConfig: finalSanitizedEditorConfig,
|
editorConfig: finalSanitizedEditorConfig,
|
||||||
generateComponentMap: getGenerateComponentMap({
|
generateComponentMap: getGenerateComponentMap({
|
||||||
resolvedFeatureMap: resolvedFeatureMap,
|
resolvedFeatureMap,
|
||||||
|
}),
|
||||||
|
generateSchemaMap: getGenerateSchemaMap({
|
||||||
|
resolvedFeatureMap,
|
||||||
}),
|
}),
|
||||||
outputSchema: ({ field, interfaceNameDefinitions, isRequired }) => {
|
outputSchema: ({ field, interfaceNameDefinitions, isRequired }) => {
|
||||||
let outputSchema: JSONSchema4 = {
|
let outputSchema: JSONSchema4 = {
|
||||||
@@ -234,23 +243,6 @@ export {
|
|||||||
} from './field/features/Blocks/nodes/BlocksNode'
|
} from './field/features/Blocks/nodes/BlocksNode'
|
||||||
export { HeadingFeature } from './field/features/Heading'
|
export { HeadingFeature } from './field/features/Heading'
|
||||||
|
|
||||||
export { LinkFeature } from './field/features/Link'
|
|
||||||
export type { LinkFeatureProps } from './field/features/Link'
|
|
||||||
export {
|
|
||||||
$createAutoLinkNode,
|
|
||||||
$isAutoLinkNode,
|
|
||||||
AutoLinkNode,
|
|
||||||
type SerializedAutoLinkNode,
|
|
||||||
} from './field/features/Link/nodes/AutoLinkNode'
|
|
||||||
export {
|
|
||||||
$createLinkNode,
|
|
||||||
$isLinkNode,
|
|
||||||
type LinkFields,
|
|
||||||
LinkNode,
|
|
||||||
type SerializedLinkNode,
|
|
||||||
TOGGLE_LINK_COMMAND,
|
|
||||||
} from './field/features/Link/nodes/LinkNode'
|
|
||||||
|
|
||||||
export { ParagraphFeature } from './field/features/Paragraph'
|
export { ParagraphFeature } from './field/features/Paragraph'
|
||||||
export { RelationshipFeature } from './field/features/Relationship'
|
export { RelationshipFeature } from './field/features/Relationship'
|
||||||
export {
|
export {
|
||||||
@@ -260,6 +252,7 @@ export {
|
|||||||
RelationshipNode,
|
RelationshipNode,
|
||||||
type SerializedRelationshipNode,
|
type SerializedRelationshipNode,
|
||||||
} from './field/features/Relationship/nodes/RelationshipNode'
|
} from './field/features/Relationship/nodes/RelationshipNode'
|
||||||
|
|
||||||
export { UploadFeature } from './field/features/Upload'
|
export { UploadFeature } from './field/features/Upload'
|
||||||
export type { UploadFeatureProps } from './field/features/Upload'
|
export type { UploadFeatureProps } from './field/features/Upload'
|
||||||
export type { RawUploadPayload } from './field/features/Upload/nodes/UploadNode'
|
export type { RawUploadPayload } from './field/features/Upload/nodes/UploadNode'
|
||||||
@@ -270,7 +263,7 @@ export {
|
|||||||
type UploadData,
|
type UploadData,
|
||||||
UploadNode,
|
UploadNode,
|
||||||
} from './field/features/Upload/nodes/UploadNode'
|
} from './field/features/Upload/nodes/UploadNode'
|
||||||
export { AlignFeature } from './field/features/align'
|
export { AlignFeature } from './field/features/align/feature.server'
|
||||||
export { TextDropdownSectionWithEntries } from './field/features/common/floatingSelectToolbarTextDropdownSection'
|
export { TextDropdownSectionWithEntries } from './field/features/common/floatingSelectToolbarTextDropdownSection'
|
||||||
export {
|
export {
|
||||||
HTMLConverterFeature,
|
HTMLConverterFeature,
|
||||||
@@ -287,19 +280,36 @@ export { defaultHTMLConverters } from './field/features/converters/html/converte
|
|||||||
export type { HTMLConverter } from './field/features/converters/html/converter/types'
|
export type { HTMLConverter } from './field/features/converters/html/converter/types'
|
||||||
export { consolidateHTMLConverters } from './field/features/converters/html/field'
|
export { consolidateHTMLConverters } from './field/features/converters/html/field'
|
||||||
export { lexicalHTML } from './field/features/converters/html/field'
|
export { lexicalHTML } from './field/features/converters/html/field'
|
||||||
|
|
||||||
export { TestRecorderFeature } from './field/features/debug/TestRecorder'
|
export { TestRecorderFeature } from './field/features/debug/TestRecorder'
|
||||||
export { TreeViewFeature } from './field/features/debug/TreeView'
|
export { TreeViewFeature } from './field/features/debug/TreeView'
|
||||||
|
|
||||||
export { BoldTextFeature } from './field/features/format/Bold'
|
export { BoldTextFeature } from './field/features/format/Bold'
|
||||||
|
|
||||||
export { InlineCodeTextFeature } from './field/features/format/InlineCode'
|
export { InlineCodeTextFeature } from './field/features/format/InlineCode'
|
||||||
export { ItalicTextFeature } from './field/features/format/Italic'
|
export { ItalicTextFeature } from './field/features/format/Italic'
|
||||||
|
|
||||||
export { SectionWithEntries as FormatSectionWithEntries } from './field/features/format/common/floatingSelectToolbarSection'
|
export { SectionWithEntries as FormatSectionWithEntries } from './field/features/format/common/floatingSelectToolbarSection'
|
||||||
export { StrikethroughTextFeature } from './field/features/format/strikethrough'
|
export { StrikethroughTextFeature } from './field/features/format/strikethrough'
|
||||||
export { SubscriptTextFeature } from './field/features/format/subscript'
|
export { SubscriptTextFeature } from './field/features/format/subscript'
|
||||||
export { SuperscriptTextFeature } from './field/features/format/superscript'
|
export { SuperscriptTextFeature } from './field/features/format/superscript'
|
||||||
export { UnderlineTextFeature } from './field/features/format/underline'
|
export { UnderlineTextFeature } from './field/features/format/underline'
|
||||||
export { IndentFeature } from './field/features/indent'
|
export { IndentFeature } from './field/features/indent'
|
||||||
|
export { LinkFeature, type LinkFeatureServerProps } from './field/features/link/feature.server'
|
||||||
|
export {
|
||||||
|
$createAutoLinkNode,
|
||||||
|
$isAutoLinkNode,
|
||||||
|
AutoLinkNode,
|
||||||
|
} from './field/features/link/nodes/AutoLinkNode'
|
||||||
|
export {
|
||||||
|
$createLinkNode,
|
||||||
|
$isLinkNode,
|
||||||
|
LinkNode,
|
||||||
|
TOGGLE_LINK_COMMAND,
|
||||||
|
} from './field/features/link/nodes/LinkNode'
|
||||||
|
export type {
|
||||||
|
LinkFields,
|
||||||
|
SerializedAutoLinkNode,
|
||||||
|
SerializedLinkNode,
|
||||||
|
} from './field/features/link/nodes/types'
|
||||||
export { CheckListFeature } from './field/features/lists/CheckList'
|
export { CheckListFeature } from './field/features/lists/CheckList'
|
||||||
export { OrderedListFeature } from './field/features/lists/OrderedList'
|
export { OrderedListFeature } from './field/features/lists/OrderedList'
|
||||||
export { UnorderedListFeature } from './field/features/lists/UnorderedList'
|
export { UnorderedListFeature } from './field/features/lists/UnorderedList'
|
||||||
@@ -330,26 +340,50 @@ export type {
|
|||||||
} from './field/features/migrations/SlateToLexical/converter/types'
|
} from './field/features/migrations/SlateToLexical/converter/types'
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
Feature,
|
ClientFeature,
|
||||||
FeatureProvider,
|
ClientFeatureProviderMap,
|
||||||
FeatureProviderMap,
|
FeatureProviderClient,
|
||||||
|
FeatureProviderProviderClient,
|
||||||
|
FeatureProviderProviderServer,
|
||||||
|
FeatureProviderServer,
|
||||||
NodeValidation,
|
NodeValidation,
|
||||||
PopulationPromise,
|
PopulationPromise,
|
||||||
ResolvedFeature,
|
ResolvedClientFeature,
|
||||||
ResolvedFeatureMap,
|
ResolvedClientFeatureMap,
|
||||||
SanitizedFeatures,
|
ResolvedServerFeature,
|
||||||
|
ResolvedServerFeatureMap,
|
||||||
|
SanitizedClientFeatures,
|
||||||
|
SanitizedPlugin,
|
||||||
|
SanitizedServerFeatures,
|
||||||
|
ServerFeature,
|
||||||
|
ServerFeatureProviderMap,
|
||||||
} from './field/features/types'
|
} from './field/features/types'
|
||||||
export {
|
export {
|
||||||
EditorConfigProvider,
|
EditorConfigProvider,
|
||||||
useEditorConfigContext,
|
useEditorConfigContext,
|
||||||
} from './field/lexical/config/EditorConfigProvider'
|
} from './field/lexical/config/client/EditorConfigProvider'
|
||||||
|
export {
|
||||||
|
sanitizeClientEditorConfig,
|
||||||
|
sanitizeClientFeatures,
|
||||||
|
} from './field/lexical/config/client/sanitize'
|
||||||
export {
|
export {
|
||||||
defaultEditorConfig,
|
defaultEditorConfig,
|
||||||
defaultEditorFeatures,
|
defaultEditorFeatures,
|
||||||
defaultSanitizedEditorConfig,
|
defaultEditorLexicalConfig,
|
||||||
} from './field/lexical/config/default'
|
defaultSanitizedServerEditorConfig,
|
||||||
export { loadFeatures, sortFeaturesForOptimalLoading } from './field/lexical/config/loader'
|
} from './field/lexical/config/server/default'
|
||||||
export { sanitizeEditorConfig, sanitizeFeatures } from './field/lexical/config/sanitize'
|
export { loadFeatures, sortFeaturesForOptimalLoading } from './field/lexical/config/server/loader'
|
||||||
|
export {
|
||||||
|
sanitizeServerEditorConfig,
|
||||||
|
sanitizeServerFeatures,
|
||||||
|
} from './field/lexical/config/server/sanitize'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ClientEditorConfig,
|
||||||
|
SanitizedClientEditorConfig,
|
||||||
|
SanitizedServerEditorConfig,
|
||||||
|
ServerEditorConfig,
|
||||||
|
} from './field/lexical/config/types'
|
||||||
export { getEnabledNodes } from './field/lexical/nodes'
|
export { getEnabledNodes } from './field/lexical/nodes'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -357,8 +391,6 @@ export {
|
|||||||
type FloatingToolbarSectionEntry,
|
type FloatingToolbarSectionEntry,
|
||||||
} from './field/lexical/plugins/FloatingSelectToolbar/types'
|
} from './field/lexical/plugins/FloatingSelectToolbar/types'
|
||||||
export { ENABLE_SLASH_MENU_COMMAND } from './field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/index'
|
export { ENABLE_SLASH_MENU_COMMAND } from './field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/index'
|
||||||
// export SanitizedEditorConfig
|
|
||||||
export type { EditorConfig, SanitizedEditorConfig }
|
|
||||||
export type { AdapterProps }
|
export type { AdapterProps }
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import type { SerializedEditorState } from 'lexical'
|
|||||||
import type { FieldPermissions } from 'payload/auth'
|
import type { FieldPermissions } from 'payload/auth'
|
||||||
import type { FieldTypes } from 'payload/config'
|
import type { FieldTypes } from 'payload/config'
|
||||||
import type { RichTextFieldProps } from 'payload/types'
|
import type { RichTextFieldProps } from 'payload/types'
|
||||||
|
import type React from 'react'
|
||||||
|
|
||||||
import type { SanitizedEditorConfig } from './field/lexical/config/types'
|
import type { SanitizedServerEditorConfig } from './field/lexical/config/types'
|
||||||
|
|
||||||
export type FieldProps = RichTextFieldProps<SerializedEditorState, AdapterProps, AdapterProps> & {
|
export type FieldProps = RichTextFieldProps<SerializedEditorState, AdapterProps, AdapterProps> & {
|
||||||
fieldTypes: FieldTypes
|
fieldTypes: FieldTypes
|
||||||
@@ -13,5 +14,11 @@ export type FieldProps = RichTextFieldProps<SerializedEditorState, AdapterProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AdapterProps = {
|
export type AdapterProps = {
|
||||||
editorConfig: SanitizedEditorConfig
|
editorConfig: SanitizedServerEditorConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GeneratedFeatureProviderComponent = {
|
||||||
|
ClientComponent: React.ReactNode
|
||||||
|
key: string
|
||||||
|
order: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import type { FeatureProvider } from './field/features/types'
|
|||||||
|
|
||||||
import { useFieldPath } from '../../ui/src/forms/FieldPathProvider'
|
import { useFieldPath } from '../../ui/src/forms/FieldPathProvider'
|
||||||
|
|
||||||
type FeatureProviderGetter = () => FeatureProvider
|
export const useLexicalFeature = (key: string, feature: FeatureProvider) => {
|
||||||
|
|
||||||
export const useLexicalFeature = (key: string, feature: FeatureProviderGetter) => {
|
|
||||||
const { schemaPath } = useFieldPath()
|
const { schemaPath } = useFieldPath()
|
||||||
|
|
||||||
useAddClientFunction(`lexicalFeature.${schemaPath}.${key}`, feature)
|
useAddClientFunction(`lexicalFeature.${schemaPath}.${key}`, feature)
|
||||||
|
|||||||
@@ -21,5 +21,5 @@
|
|||||||
"src/**/*.spec.tsx"
|
"src/**/*.spec.tsx"
|
||||||
],
|
],
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
|
||||||
"references": [{ "path": "../payload" }]
|
"references": [{ "path": "../payload" }, { "path": "../translations" }, { "path": "../ui" }]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const LinkElement = () => {
|
|||||||
const fieldMapPath = `${schemaPath}.${linkFieldsSchemaPath}`
|
const fieldMapPath = `${schemaPath}.${linkFieldsSchemaPath}`
|
||||||
|
|
||||||
const { richTextComponentMap } = fieldProps
|
const { richTextComponentMap } = fieldProps
|
||||||
const fieldMap = richTextComponentMap.get(fieldMapPath)
|
const fieldMap = richTextComponentMap.get(linkFieldsSchemaPath)
|
||||||
|
|
||||||
const editor = useSlate()
|
const editor = useSlate()
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
@@ -113,7 +113,7 @@ export const LinkElement = () => {
|
|||||||
setInitialState(state)
|
setInitialState(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
awaitInitialState()
|
void awaitInitialState()
|
||||||
}, [renderModal, element, user, locale, t, getDocPreferences, config, id, fieldMapPath])
|
}, [renderModal, element, user, locale, t, getDocPreferences, config, id, fieldMapPath])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import type { Editor } from 'slate'
|
|||||||
|
|
||||||
import { useSlatePlugin } from '../../../utilities/useSlatePlugin'
|
import { useSlatePlugin } from '../../../utilities/useSlatePlugin'
|
||||||
|
|
||||||
export const WithLinks: React.FC = () => {
|
const plugin = (incomingEditor: Editor): Editor => {
|
||||||
useSlatePlugin('withLinks', (incomingEditor: Editor): Editor => {
|
|
||||||
const editor = incomingEditor
|
const editor = incomingEditor
|
||||||
const { isInline } = editor
|
const { isInline } = editor
|
||||||
|
|
||||||
@@ -19,6 +18,9 @@ export const WithLinks: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return editor
|
return editor
|
||||||
})
|
}
|
||||||
|
|
||||||
|
export const WithLinks: React.FC = () => {
|
||||||
|
useSlatePlugin('withLinks', plugin)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export declare function transpileAndCopy(sourcePath: any, targetPath: any): Promise<void>;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.transpileAndCopy = void 0;
|
|
||||||
const fs_1 = __importDefault(require("fs"));
|
|
||||||
const swc = require('@swc/core');
|
|
||||||
async function transpileAndCopy(sourcePath, targetPath) {
|
|
||||||
try {
|
|
||||||
const inputCode = fs_1.default.readFileSync(sourcePath, 'utf-8');
|
|
||||||
const { code } = await swc.transform(inputCode, {
|
|
||||||
filename: sourcePath,
|
|
||||||
jsc: {
|
|
||||||
parser: {
|
|
||||||
syntax: 'typescript',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
fs_1.default.writeFileSync(targetPath.replace(/\.tsx?$/, '.js'), code, 'utf-8');
|
|
||||||
console.log(`Transpiled and copied ${sourcePath} to ${targetPath.replace(/\.tsx?$/, '.js')}`);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error(`Error transpiling ${sourcePath}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.transpileAndCopy = transpileAndCopy;
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export { default as Error } from '../forms/Error'
|
export { default as Error } from '../forms/Error'
|
||||||
export { default as FieldDescription } from '../forms/FieldDescription'
|
export { default as FieldDescription } from '../forms/FieldDescription'
|
||||||
export { FieldPathProvider } from '../forms/FieldPathProvider'
|
export { FieldPathProvider } from '../forms/FieldPathProvider'
|
||||||
|
export { useFieldPath } from '../forms/FieldPathProvider'
|
||||||
export { default as Form } from '../forms/Form'
|
export { default as Form } from '../forms/Form'
|
||||||
export { default as buildInitialState } from '../forms/Form'
|
export { default as buildInitialState } from '../forms/Form'
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const useAddClientFunction = (key: string, func: any) => {
|
|||||||
func,
|
func,
|
||||||
key,
|
key,
|
||||||
})
|
})
|
||||||
}, [func, key])
|
}, [func, key, addClientFunction])
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useClientFunctions = () => {
|
export const useClientFunctions = () => {
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -1205,6 +1205,9 @@ importers:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 18.2.15
|
specifier: 18.2.15
|
||||||
version: 18.2.15
|
version: 18.2.15
|
||||||
|
'@types/react-dom':
|
||||||
|
specifier: 18.2.7
|
||||||
|
version: 18.2.7
|
||||||
payload:
|
payload:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../payload
|
version: link:../payload
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlignFeature, LinkFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
import type { Config, SanitizedConfig } from '../packages/payload/src/config/types'
|
import type { Config, SanitizedConfig } from '../packages/payload/src/config/types'
|
||||||
@@ -5,7 +6,7 @@ import type { Config, SanitizedConfig } from '../packages/payload/src/config/typ
|
|||||||
import { mongooseAdapter } from '../packages/db-mongodb/src'
|
import { mongooseAdapter } from '../packages/db-mongodb/src'
|
||||||
import { postgresAdapter } from '../packages/db-postgres/src'
|
import { postgresAdapter } from '../packages/db-postgres/src'
|
||||||
import { buildConfig as buildPayloadConfig } from '../packages/payload/src/config/build'
|
import { buildConfig as buildPayloadConfig } from '../packages/payload/src/config/build'
|
||||||
import { slateEditor } from '../packages/richtext-slate/src'
|
//import { slateEditor } from '../packages/richtext-slate/src'
|
||||||
|
|
||||||
// process.env.PAYLOAD_DATABASE = 'postgres'
|
// process.env.PAYLOAD_DATABASE = 'postgres'
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
|
|||||||
const config: Config = {
|
const config: Config = {
|
||||||
db: databaseAdapters[process.env.PAYLOAD_DATABASE || 'mongoose'],
|
db: databaseAdapters[process.env.PAYLOAD_DATABASE || 'mongoose'],
|
||||||
secret: 'TEST_SECRET',
|
secret: 'TEST_SECRET',
|
||||||
editor: slateEditor({
|
/*editor: slateEditor({
|
||||||
admin: {
|
admin: {
|
||||||
upload: {
|
upload: {
|
||||||
collections: {
|
collections: {
|
||||||
@@ -70,6 +71,9 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
}),*/
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: [LinkFeature({}), AlignFeature()],
|
||||||
}),
|
}),
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
max: 9999999999,
|
max: 9999999999,
|
||||||
|
|||||||
Reference in New Issue
Block a user