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",
|
||||
// Load .git-blame-ignore-revs file
|
||||
"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]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
|
||||
@@ -46,12 +46,13 @@
|
||||
"@types/json-schema": "7.0.15",
|
||||
"@types/node": "20.6.2",
|
||||
"@types/react": "18.2.15",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"payload": "^2.4.0",
|
||||
"@payloadcms/translations": "workspace:^",
|
||||
"@payloadcms/ui": "workspace:^"
|
||||
"@payloadcms/ui": "workspace:^",
|
||||
"payload": "^2.4.0"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
|
||||
@@ -6,12 +6,12 @@ import { useTableCell } from '@payloadcms/ui/elements'
|
||||
import { $getRoot } from 'lexical'
|
||||
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 { useClientFunctions } from '../../../ui/src/providers/ClientFunction'
|
||||
import { defaultEditorLexicalConfig } from '../field/lexical/config/defaultClient'
|
||||
import { sanitizeEditorConfig } from '../field/lexical/config/sanitize'
|
||||
import { defaultEditorLexicalConfig } from '../field/lexical/config/client/default'
|
||||
import { sanitizeClientEditorConfig } from '../field/lexical/config/client/sanitize'
|
||||
import { getEnabledNodes } from '../field/lexical/nodes'
|
||||
|
||||
export const RichTextCell: React.FC<{
|
||||
@@ -30,12 +30,10 @@ export const RichTextCell: React.FC<{
|
||||
return
|
||||
}
|
||||
|
||||
const finalSanitizedEditorConfig: SanitizedEditorConfig = sanitizeEditorConfig({
|
||||
features: [],
|
||||
lexical: lexicalEditorConfig
|
||||
? () => Promise.resolve(lexicalEditorConfig)
|
||||
: () => Promise.resolve(defaultEditorLexicalConfig),
|
||||
})
|
||||
const finalSanitizedEditorConfig: SanitizedClientEditorConfig = sanitizeClientEditorConfig(
|
||||
lexicalEditorConfig ? lexicalEditorConfig : defaultEditorLexicalConfig,
|
||||
null,
|
||||
)
|
||||
|
||||
// Transform data through load hooks
|
||||
if (finalSanitizedEditorConfig?.features?.hooks?.load?.length) {
|
||||
@@ -61,23 +59,21 @@ export const RichTextCell: React.FC<{
|
||||
return
|
||||
}
|
||||
|
||||
void finalSanitizedEditorConfig.lexical().then((lexicalConfig: LexicalEditorConfig) => {
|
||||
// initialize headless editor
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
namespace: lexicalConfig.namespace,
|
||||
nodes: getEnabledNodes({ editorConfig: finalSanitizedEditorConfig }),
|
||||
theme: lexicalConfig.theme,
|
||||
})
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse))
|
||||
|
||||
const textContent =
|
||||
headlessEditor.getEditorState().read(() => {
|
||||
return $getRoot().getTextContent()
|
||||
}) || ''
|
||||
|
||||
// Limiting the number of characters shown is done in a CSS rule
|
||||
setPreview(textContent)
|
||||
// initialize headless editor
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
namespace: finalSanitizedEditorConfig.lexical.namespace,
|
||||
nodes: getEnabledNodes({ editorConfig: finalSanitizedEditorConfig }),
|
||||
theme: finalSanitizedEditorConfig.lexical.theme,
|
||||
})
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse))
|
||||
|
||||
const textContent =
|
||||
headlessEditor.getEditorState().read(() => {
|
||||
return $getRoot().getTextContent()
|
||||
}) || ''
|
||||
|
||||
// Limiting the number of characters shown is done in a CSS rule
|
||||
setPreview(textContent)
|
||||
}, [cellData, lexicalEditorConfig])
|
||||
|
||||
return <span>{preview}</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export { RichTextCell } from '../cell'
|
||||
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 { 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 { ErrorBoundary } from 'react-error-boundary'
|
||||
|
||||
import type { SanitizedEditorConfig } from './lexical/config/types'
|
||||
import type { SanitizedClientEditorConfig } from './lexical/config/types'
|
||||
|
||||
import { richTextValidateHOC } from '../validate'
|
||||
import './index.scss'
|
||||
@@ -15,7 +15,7 @@ const baseClass = 'rich-text-lexical'
|
||||
|
||||
const RichText: React.FC<
|
||||
FormFieldBase & {
|
||||
editorConfig: SanitizedEditorConfig // With rendered features n stuff
|
||||
editorConfig: SanitizedClientEditorConfig // With rendered features n stuff
|
||||
name: string
|
||||
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) {
|
||||
// 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 { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider'
|
||||
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider'
|
||||
import { transformInputFormSchema } from '../utils/transformInputFormSchema'
|
||||
import { BlockContent } from './BlockContent'
|
||||
import './index.scss'
|
||||
|
||||
@@ -13,7 +13,7 @@ import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { BlocksFeatureProps } from '..'
|
||||
|
||||
import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider'
|
||||
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider'
|
||||
import { $createBlockNode } from '../nodes/BlocksNode'
|
||||
import { INSERT_BLOCK_COMMAND } from '../plugin/commands'
|
||||
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 { useEditorConfigContext } from '../../../../lexical/config/EditorConfigProvider'
|
||||
import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider'
|
||||
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from '../../drawer/commands'
|
||||
import './index.scss'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '@payloadcms/ui/scss';
|
||||
@import '../../../../../../../ui/src/scss/styles.scss';
|
||||
|
||||
.lexical-relationship {
|
||||
@extend %body;
|
||||
|
||||
@@ -26,7 +26,7 @@ import type { ElementProps } from '..'
|
||||
import type { UploadFeatureProps } from '../..'
|
||||
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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '@payloadcms/ui/scss';
|
||||
@import '../../../../../../ui/src/scss/styles.scss';
|
||||
|
||||
.lexical-upload {
|
||||
@extend %body;
|
||||
|
||||
@@ -21,7 +21,7 @@ import React, { useCallback, useReducer, useState } from 'react'
|
||||
import type { UploadFeatureProps } from '..'
|
||||
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 { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from '../drawer/commands'
|
||||
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,
|
||||
} from '../../lexical/plugins/FloatingSelectToolbar/types'
|
||||
|
||||
import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft'
|
||||
|
||||
export const AlignDropdownSectionWithEntries = (
|
||||
entries: FloatingToolbarSectionEntry[],
|
||||
): FloatingToolbarSection => {
|
||||
return {
|
||||
type: 'dropdown',
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error-next-line
|
||||
import('../../lexical/ui/icons/AlignLeft').then((module) => module.AlignLeftIcon),
|
||||
ChildComponent: AlignLeftIcon,
|
||||
entries,
|
||||
key: 'dropdown-align',
|
||||
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 {
|
||||
&__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 Field } from 'payload/types'
|
||||
import type { LinkPayload } from '../plugins/floatingLinkEditor/types'
|
||||
|
||||
export interface Props {
|
||||
drawerSlug: string
|
||||
fieldSchema: Field[]
|
||||
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,
|
||||
} 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
|
||||
// allow typing within the link
|
||||
@@ -15,39 +15,11 @@ import {
|
||||
type LexicalNode,
|
||||
type NodeKey,
|
||||
type RangeSelection,
|
||||
type SerializedElementNode,
|
||||
type Spread,
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
|
||||
import type { LinkPayload } from '../plugins/floatingLinkEditor/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
|
||||
>
|
||||
import type { LinkFields, SerializedLinkNode } from './types'
|
||||
|
||||
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'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import type { LinkFields } from '../../nodes/types'
|
||||
|
||||
import { invariant } from '../../../../lexical/utils/invariant'
|
||||
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
|
||||
|
||||
@@ -6,16 +6,7 @@ import { useModal } from '@faceless-ui/modal'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { $findMatchingParent, mergeRegister } from '@lexical/utils'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import {
|
||||
buildStateFromSchema,
|
||||
formatDrawerSlug,
|
||||
useAuth,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useEditDepth,
|
||||
useLocale,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { formatDrawerSlug, useConfig, useEditDepth, useTranslation } from '@payloadcms/ui'
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
@@ -24,29 +15,21 @@ import {
|
||||
KEY_ESCAPE_COMMAND,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
} from 'lexical'
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import type { LinkFeatureProps } from '../../..'
|
||||
import type { LinkNode } from '../../../nodes/LinkNode'
|
||||
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 { setFloatingElemPositionForLinkEditor } from '../../../../../lexical/utils/setFloatingElemPositionForLinkEditor'
|
||||
import { LinkDrawer } from '../../../drawer'
|
||||
import { $isAutoLinkNode } from '../../../nodes/AutoLinkNode'
|
||||
import { $createLinkNode } from '../../../nodes/LinkNode'
|
||||
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '../../../nodes/LinkNode'
|
||||
import { transformExtraFields } from '../utilities'
|
||||
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './commands'
|
||||
|
||||
export function LinkEditor({
|
||||
anchorElem,
|
||||
disabledCollections,
|
||||
enabledCollections,
|
||||
fields: customFieldSchema,
|
||||
}: { anchorElem: HTMLElement } & LinkFeatureProps): JSX.Element {
|
||||
export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.ReactNode {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const editorRef = useRef<HTMLDivElement | null>(null)
|
||||
@@ -57,36 +40,9 @@ export function LinkEditor({
|
||||
|
||||
const config = useConfig()
|
||||
|
||||
const { user } = useAuth()
|
||||
const { code: locale } = useLocale()
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const { getDocPreferences } = useDocumentInfo()
|
||||
|
||||
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 [stateData, setStateData] = useState<LinkPayload>(null)
|
||||
|
||||
const { closeModal, toggleModal } = useModal()
|
||||
const editDepth = useEditDepth()
|
||||
@@ -98,7 +54,7 @@ export function LinkEditor({
|
||||
depth: editDepth,
|
||||
})
|
||||
|
||||
const updateLinkEditor = useCallback(async () => {
|
||||
const updateLinkEditor = useCallback(() => {
|
||||
const selection = $getSelection()
|
||||
let selectedNodeDomRect: DOMRect | undefined = null
|
||||
|
||||
@@ -147,22 +103,7 @@ export function LinkEditor({
|
||||
setLinkLabel(label)
|
||||
}
|
||||
|
||||
// Set initial state of the drawer. This will basically pre-fill the drawer fields with the
|
||||
// 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)
|
||||
setStateData(data)
|
||||
setIsLink(true)
|
||||
if ($isAutoLinkNode(linkParent)) {
|
||||
setIsAutoLink(true)
|
||||
@@ -206,7 +147,7 @@ export function LinkEditor({
|
||||
}
|
||||
|
||||
return true
|
||||
}, [anchorElem, editor, fieldSchema, config, getDocPreferences, locale, t, user, i18n])
|
||||
}, [anchorElem, editor, config, t, i18n])
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
@@ -217,12 +158,8 @@ export function LinkEditor({
|
||||
|
||||
// Now, open the modal
|
||||
updateLinkEditor()
|
||||
.then(() => {
|
||||
toggleModal(drawerSlug)
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error
|
||||
})
|
||||
toggleModal(drawerSlug)
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
@@ -339,7 +276,6 @@ export function LinkEditor({
|
||||
</div>
|
||||
<LinkDrawer
|
||||
drawerSlug={drawerSlug}
|
||||
fieldSchema={fieldSchema}
|
||||
handleModalSubmit={(fields: FormState, data: Data) => {
|
||||
closeModal(drawerSlug)
|
||||
|
||||
@@ -363,7 +299,7 @@ export function LinkEditor({
|
||||
// it being applied to the auto link node instead of the link node.
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, newLinkPayload)
|
||||
}}
|
||||
initialState={initialState}
|
||||
stateData={stateData}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '@payloadcms/ui/scss';
|
||||
@import '../../../../../../../ui/src/scss/styles.scss';
|
||||
|
||||
html[data-theme='light'] {
|
||||
.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
|
||||
@@ -10,10 +10,11 @@ import {
|
||||
} from 'lexical'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import type { LinkFields } from '../../nodes/types'
|
||||
import type { LinkPayload } from '../floatingLinkEditor/types'
|
||||
|
||||
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 {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { LinkFeatureProps } from '.'
|
||||
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 { recurseNestedFields } from '../../../populate/recurseNestedFields'
|
||||
|
||||
export const linkPopulationPromiseHOC = (
|
||||
props: LinkFeatureProps,
|
||||
props: LinkFeatureServerProps,
|
||||
): PopulationPromise<SerializedLinkNode> => {
|
||||
const linkPopulationPromise: PopulationPromise<SerializedLinkNode> = ({
|
||||
context,
|
||||
@@ -1,5 +1,5 @@
|
||||
/* 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 { 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 { convertSlateNodesToLexical } from '..'
|
||||
|
||||
@@ -5,11 +5,11 @@ import type { SerializedLexicalNode } from 'lexical'
|
||||
import type { LexicalNodeReplacement } from 'lexical'
|
||||
import type { RequestContext } from 'payload'
|
||||
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 { 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 { SlashMenuGroup } from '../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/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
|
||||
|
||||
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?: {
|
||||
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?: {
|
||||
modifyOutputSchema: ({
|
||||
currentSchema,
|
||||
@@ -95,16 +221,6 @@ export type Feature = {
|
||||
incomingEditorState: SerializedEditorState
|
||||
siblingDoc: Record<string, unknown>
|
||||
}) => Promise<void> | null
|
||||
load?: ({
|
||||
incomingEditorState,
|
||||
}: {
|
||||
incomingEditorState: SerializedEditorState
|
||||
}) => SerializedEditorState
|
||||
save?: ({
|
||||
incomingEditorState,
|
||||
}: {
|
||||
incomingEditorState: SerializedEditorState
|
||||
}) => SerializedEditorState
|
||||
}
|
||||
markdownTransformers?: Transformer[]
|
||||
nodes?: Array<{
|
||||
@@ -113,107 +229,66 @@ export type Feature = {
|
||||
}
|
||||
node: Klass<LexicalNode> | LexicalNodeReplacement
|
||||
populationPromises?: Array<PopulationPromise>
|
||||
type: string
|
||||
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: unknown
|
||||
slashMenu?: {
|
||||
dynamicOptions?: ({
|
||||
editor,
|
||||
queryString,
|
||||
}: {
|
||||
editor: LexicalEditor
|
||||
queryString: string
|
||||
}) => SlashMenuGroup[]
|
||||
options?: SlashMenuGroup[]
|
||||
}
|
||||
serverFeatureProps: ServerProps
|
||||
}
|
||||
|
||||
export type FeatureProvider = {
|
||||
Component: React.FC
|
||||
/** 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. 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 &
|
||||
export type ResolvedServerFeature<ServerProps, ClientFeatureProps> = ServerFeature<
|
||||
ServerProps,
|
||||
ClientFeatureProps
|
||||
> &
|
||||
Required<
|
||||
Pick<
|
||||
FeatureProvider,
|
||||
'Component' | 'dependencies' | 'dependenciesPriority' | 'dependenciesSoft' | 'key'
|
||||
FeatureProviderServer<ServerProps, ClientFeatureProps>,
|
||||
'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 =
|
||||
| {
|
||||
// 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 }>>
|
||||
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>
|
||||
Component: React.FC
|
||||
key: string
|
||||
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>
|
||||
Component: React.FC
|
||||
key: string
|
||||
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>
|
||||
Component: React.FC
|
||||
key: string
|
||||
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<
|
||||
Pick<ResolvedFeature, 'markdownTransformers' | 'nodes'>
|
||||
export type SanitizedServerFeatures = Required<
|
||||
Pick<ResolvedServerFeature<unknown, unknown>, 'markdownTransformers' | 'nodes'>
|
||||
> & {
|
||||
/** The node types mapped to their converters */
|
||||
converters: {
|
||||
@@ -221,9 +296,6 @@ export type SanitizedFeatures = Required<
|
||||
}
|
||||
/** The keys of all enabled features */
|
||||
enabledFeatures: string[]
|
||||
floatingSelectToolbar: {
|
||||
sections: FloatingToolbarSection[]
|
||||
}
|
||||
generatedTypes: {
|
||||
modifyOutputSchemas: Array<
|
||||
({
|
||||
@@ -257,6 +329,22 @@ export type SanitizedFeatures = Required<
|
||||
siblingDoc: Record<string, unknown>
|
||||
}) => 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<
|
||||
({
|
||||
incomingEditorState,
|
||||
@@ -273,14 +361,10 @@ export type SanitizedFeatures = Required<
|
||||
>
|
||||
}
|
||||
plugins?: Array<SanitizedPlugin>
|
||||
/** The node types mapped to their populationPromises */
|
||||
populationPromises: Map<string, Array<PopulationPromise>>
|
||||
slashMenu: {
|
||||
dynamicOptions: Array<
|
||||
({ editor, queryString }: { editor: LexicalEditor; queryString: string }) => 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 {
|
||||
display: flex;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
'use client'
|
||||
import type { FeatureProvider } from '@payloadcms/richtext-lexical'
|
||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||
|
||||
import { type FormFieldBase, ShimmerEffect } from '@payloadcms/ui'
|
||||
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 { useClientFunctions } from '../../../ui/src/providers/ClientFunction'
|
||||
import { defaultEditorLexicalConfig } from './lexical/config/defaultClient'
|
||||
import { sanitizeEditorConfig } from './lexical/config/sanitize'
|
||||
import { defaultEditorLexicalConfig } from './lexical/config/client/default'
|
||||
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
|
||||
const RichTextEditor = lazy(() => import('./Field'))
|
||||
@@ -27,20 +29,19 @@ export const RichTextField: React.FC<
|
||||
const clientFunctions = useClientFunctions()
|
||||
const [hasLoadedFeatures, setHasLoadedFeatures] = useState(false)
|
||||
|
||||
const [featureProviders, setFeatureProviders] = useState<FeatureProvider[]>([])
|
||||
const [featureProviders, setFeatureProviders] = useState<FeatureProviderClient<unknown>[]>([])
|
||||
|
||||
const finalSanitizedEditorConfig: SanitizedEditorConfig = sanitizeEditorConfig({
|
||||
features: [],
|
||||
lexical: lexicalEditorConfig
|
||||
? () => Promise.resolve(lexicalEditorConfig)
|
||||
: () => Promise.resolve(defaultEditorLexicalConfig),
|
||||
})
|
||||
const [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] =
|
||||
useState<SanitizedClientEditorConfig>(null)
|
||||
|
||||
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(() => {
|
||||
if (!hasLoadedFeatures) {
|
||||
const featureProvidersLocal: FeatureProvider[] = []
|
||||
const featureProvidersLocal: FeatureProviderClient<unknown>[] = []
|
||||
|
||||
Object.entries(clientFunctions).forEach(([key, plugin]) => {
|
||||
if (key.startsWith(`lexicalFeature.${schemaPath}.`)) {
|
||||
@@ -51,30 +52,56 @@ export const RichTextField: React.FC<
|
||||
if (featureProvidersLocal.length === featureProviderComponents.length) {
|
||||
setFeatureProviders(featureProvidersLocal)
|
||||
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) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{Array.isArray(featureProviderComponents) &&
|
||||
featureProviderComponents.map((FeatureProvider, i) => {
|
||||
return <React.Fragment key={i}>{FeatureProvider}</React.Fragment>
|
||||
featureProviderComponents.map((FeatureProvider) => {
|
||||
return (
|
||||
<React.Fragment key={FeatureProvider.key}>
|
||||
{FeatureProvider.ClientComponent}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const features = clientFunctions
|
||||
|
||||
console.log('clientFunctions', features['lexicalFeature.posts.richText.paragraph'])
|
||||
|
||||
//features['lexicalFeature.posts.richText.paragraph']()
|
||||
|
||||
return (
|
||||
<Suspense fallback={<ShimmerEffect height="35vh" />}>
|
||||
<RichTextEditor {...props} editorConfig={finalSanitizedEditorConfig} />
|
||||
{finalSanitizedEditorConfig && (
|
||||
<RichTextEditor {...props} editorConfig={finalSanitizedEditorConfig} />
|
||||
)}
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import type { SanitizedPlugin } from '../features/types'
|
||||
|
||||
@@ -7,31 +6,9 @@ export const EditorPlugin: React.FC<{
|
||||
anchorElem?: HTMLDivElement
|
||||
plugin: SanitizedPlugin
|
||||
}> = ({ 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') {
|
||||
return (
|
||||
Component && (
|
||||
<React.Suspense>
|
||||
<Component anchorElem={anchorElem} />
|
||||
</React.Suspense>
|
||||
)
|
||||
)
|
||||
return plugin.Component && <plugin.Component anchorElem={anchorElem} />
|
||||
}
|
||||
|
||||
return (
|
||||
Component && (
|
||||
<React.Suspense>
|
||||
<Component />
|
||||
</React.Suspense>
|
||||
)
|
||||
)
|
||||
return plugin.Component && <plugin.Component />
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
|
||||
{editor.isEditable() && (
|
||||
<React.Fragment>
|
||||
<HistoryPlugin />
|
||||
<MarkdownShortcutPlugin />
|
||||
{editorConfig?.features?.markdownTransformers?.length > 0 && <MarkdownShortcutPlugin />}
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
'use client'
|
||||
import type { InitialConfigType } from '@lexical/react/LexicalComposer'
|
||||
import type { FormFieldBase } from '@payloadcms/ui'
|
||||
import type { EditorState, SerializedEditorState } from 'lexical'
|
||||
import type { LexicalEditor } from 'lexical'
|
||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||
|
||||
import { LexicalComposer } from '@lexical/react/LexicalComposer'
|
||||
import * as React from 'react'
|
||||
|
||||
import type { FieldProps } from '../../types'
|
||||
import type { SanitizedEditorConfig } from './config/types'
|
||||
import type { SanitizedClientEditorConfig } from './config/types'
|
||||
|
||||
import { LexicalEditor as LexicalEditorComponent } from './LexicalEditor'
|
||||
import { EditorConfigProvider } from './config/EditorConfigProvider'
|
||||
import { EditorConfigProvider } from './config/client/EditorConfigProvider'
|
||||
import { getEnabledNodes } from './nodes'
|
||||
|
||||
export type LexicalProviderProps = {
|
||||
editorConfig: SanitizedEditorConfig
|
||||
fieldProps: FieldProps
|
||||
editorConfig: SanitizedClientEditorConfig
|
||||
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
|
||||
path: string
|
||||
readOnly: boolean
|
||||
@@ -27,21 +31,19 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
|
||||
|
||||
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(() => {
|
||||
void editorConfig.lexical().then((lexicalConfig: LexicalEditorConfig) => {
|
||||
const newInitialConfig: InitialConfigType = {
|
||||
editable: readOnly !== true,
|
||||
editorState: value != null ? JSON.stringify(value) : undefined,
|
||||
namespace: lexicalConfig.namespace,
|
||||
nodes: [...getEnabledNodes({ editorConfig })],
|
||||
onError: (error: Error) => {
|
||||
throw error
|
||||
},
|
||||
theme: lexicalConfig.theme,
|
||||
}
|
||||
setInitialConfig(newInitialConfig)
|
||||
})
|
||||
const newInitialConfig: InitialConfigType = {
|
||||
editable: readOnly !== true,
|
||||
editorState: value != null ? JSON.stringify(value) : undefined,
|
||||
namespace: editorConfig.lexical.namespace,
|
||||
nodes: [...getEnabledNodes({ editorConfig })],
|
||||
onError: (error: Error) => {
|
||||
throw error
|
||||
},
|
||||
theme: editorConfig.lexical.theme,
|
||||
}
|
||||
setInitialConfig(newInitialConfig)
|
||||
}, [editorConfig, readOnly, value])
|
||||
|
||||
if (editorConfig?.features?.hooks?.load?.length) {
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
'use client'
|
||||
import type { FormFieldBase } from '@payloadcms/ui'
|
||||
|
||||
import * as React from 'react'
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import type { FieldProps } from '../../../types'
|
||||
import type { SanitizedEditorConfig } from './types'
|
||||
import type { FieldProps } from '../../../../types'
|
||||
import type { SanitizedClientEditorConfig } from '../types'
|
||||
|
||||
// Should always produce a 20 character pseudo-random string
|
||||
function generateQuickGuid(): string {
|
||||
return Math.random().toString(36).substring(2, 12) + Math.random().toString(36).substring(2, 12)
|
||||
}
|
||||
interface ContextType {
|
||||
editorConfig: SanitizedEditorConfig
|
||||
field: FieldProps
|
||||
editorConfig: SanitizedClientEditorConfig
|
||||
field: FormFieldBase & {
|
||||
editorConfig: SanitizedClientEditorConfig // With rendered features n stuff
|
||||
name: string
|
||||
richTextComponentMap: Map<string, React.ReactNode>
|
||||
}
|
||||
uuid: string
|
||||
}
|
||||
|
||||
@@ -27,9 +33,13 @@ export const EditorConfigProvider = ({
|
||||
fieldProps,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
editorConfig: SanitizedEditorConfig
|
||||
fieldProps: FieldProps
|
||||
}): JSX.Element => {
|
||||
editorConfig: SanitizedClientEditorConfig
|
||||
fieldProps: FormFieldBase & {
|
||||
editorConfig: SanitizedClientEditorConfig // With rendered features n stuff
|
||||
name: string
|
||||
richTextComponentMap: Map<string, React.ReactNode>
|
||||
}
|
||||
}): React.ReactNode => {
|
||||
// State to store the UUID
|
||||
const [uuid, setUuid] = useState(generateQuickGuid())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||
|
||||
import { LexicalEditorTheme } from '../theme/EditorTheme'
|
||||
import { LexicalEditorTheme } from '../../theme/EditorTheme'
|
||||
|
||||
export const defaultEditorLexicalConfig: LexicalEditorConfig = {
|
||||
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'
|
||||
import type { EditorConfig, SanitizedEditorConfig } from './types'
|
||||
'use client'
|
||||
|
||||
import { loadFeatures } from './loader'
|
||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||
|
||||
export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeatures => {
|
||||
const sanitized: SanitizedFeatures = {
|
||||
converters: {
|
||||
html: [],
|
||||
},
|
||||
import type { ResolvedClientFeatureMap, SanitizedClientFeatures } from '../../../features/types'
|
||||
import type { SanitizedClientEditorConfig } from '../types'
|
||||
|
||||
export const sanitizeClientFeatures = (
|
||||
features: ResolvedClientFeatureMap,
|
||||
): SanitizedClientFeatures => {
|
||||
const sanitized: SanitizedClientFeatures = {
|
||||
enabledFeatures: [],
|
||||
floatingSelectToolbar: {
|
||||
sections: [],
|
||||
},
|
||||
generatedTypes: {
|
||||
modifyOutputSchemas: [],
|
||||
},
|
||||
|
||||
hooks: {
|
||||
afterReadPromises: [],
|
||||
load: [],
|
||||
save: [],
|
||||
},
|
||||
markdownTransformers: [],
|
||||
nodes: [],
|
||||
plugins: [],
|
||||
populationPromises: new Map(),
|
||||
slashMenu: {
|
||||
dynamicOptions: [],
|
||||
groupsWithOptions: [],
|
||||
},
|
||||
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.hooks?.load?.length) {
|
||||
sanitized.hooks.load = sanitized.hooks.load.concat(feature.hooks.load)
|
||||
}
|
||||
@@ -51,17 +42,6 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
||||
|
||||
if (feature.nodes?.length) {
|
||||
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) {
|
||||
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) {
|
||||
for (const section of feature.floatingSelectToolbar.sections) {
|
||||
@@ -169,14 +144,13 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
||||
return sanitized
|
||||
}
|
||||
|
||||
export function sanitizeEditorConfig(editorConfig: EditorConfig): SanitizedEditorConfig {
|
||||
const resolvedFeatureMap = loadFeatures({
|
||||
unSanitizedEditorConfig: editorConfig,
|
||||
})
|
||||
|
||||
export function sanitizeClientEditorConfig(
|
||||
lexical: LexicalEditorConfig,
|
||||
resolvedClientFeatureMap: ResolvedClientFeatureMap,
|
||||
): SanitizedClientEditorConfig {
|
||||
return {
|
||||
features: sanitizeFeatures(resolvedFeatureMap),
|
||||
lexical: editorConfig.lexical,
|
||||
resolvedFeatureMap: resolvedFeatureMap,
|
||||
features: sanitizeClientFeatures(resolvedClientFeatureMap),
|
||||
lexical,
|
||||
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 { EditorConfig } from './types'
|
||||
import type {
|
||||
FeatureProviderServer,
|
||||
ResolvedServerFeatureMap,
|
||||
ServerFeatureProviderMap,
|
||||
} from '../../../features/types'
|
||||
import type { ServerEditorConfig } from '../types'
|
||||
|
||||
type DependencyGraph = {
|
||||
[key: string]: {
|
||||
dependencies: string[]
|
||||
dependenciesPriority: string[]
|
||||
dependenciesSoft: string[]
|
||||
featureProvider: FeatureProvider
|
||||
featureProvider: FeatureProviderServer<unknown, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
function createDependencyGraph(featureProviders: FeatureProvider[]): DependencyGraph {
|
||||
function createDependencyGraph(
|
||||
featureProviders: FeatureProviderServer<unknown, unknown>[],
|
||||
): DependencyGraph {
|
||||
const graph: DependencyGraph = {}
|
||||
for (const fp of featureProviders) {
|
||||
graph[fp.key] = {
|
||||
@@ -23,10 +29,12 @@ function createDependencyGraph(featureProviders: FeatureProvider[]): DependencyG
|
||||
return graph
|
||||
}
|
||||
|
||||
function topologicallySortFeatures(featureProviders: FeatureProvider[]): FeatureProvider[] {
|
||||
function topologicallySortFeatures(
|
||||
featureProviders: FeatureProviderServer<unknown, unknown>[],
|
||||
): FeatureProviderServer<unknown, unknown>[] {
|
||||
const graph = createDependencyGraph(featureProviders)
|
||||
const visited: { [key: string]: boolean } = {}
|
||||
const stack: FeatureProvider[] = []
|
||||
const stack: FeatureProviderServer<unknown, unknown>[] = []
|
||||
|
||||
for (const key in graph) {
|
||||
if (!visited[key]) {
|
||||
@@ -41,7 +49,7 @@ function visit(
|
||||
graph: DependencyGraph,
|
||||
key: string,
|
||||
visited: { [key: string]: boolean },
|
||||
stack: FeatureProvider[],
|
||||
stack: FeatureProviderServer<unknown, unknown>[],
|
||||
currentPath: string[] = [],
|
||||
) {
|
||||
if (!graph[key]) {
|
||||
@@ -90,16 +98,16 @@ function visit(
|
||||
}
|
||||
|
||||
export function sortFeaturesForOptimalLoading(
|
||||
featureProviders: FeatureProvider[],
|
||||
): FeatureProvider[] {
|
||||
featureProviders: FeatureProviderServer<unknown, unknown>[],
|
||||
): FeatureProviderServer<unknown, unknown>[] {
|
||||
return topologicallySortFeatures(featureProviders)
|
||||
}
|
||||
|
||||
export function loadFeatures({
|
||||
unSanitizedEditorConfig,
|
||||
}: {
|
||||
unSanitizedEditorConfig: EditorConfig
|
||||
}): ResolvedFeatureMap {
|
||||
unSanitizedEditorConfig: ServerEditorConfig
|
||||
}): ResolvedServerFeatureMap {
|
||||
// First remove all duplicate features. The LAST feature with a given key wins.
|
||||
unSanitizedEditorConfig.features = unSanitizedEditorConfig.features
|
||||
.reverse()
|
||||
@@ -109,15 +117,18 @@ export function loadFeatures({
|
||||
})
|
||||
.reverse()
|
||||
|
||||
const featureProviderMap: FeatureProviderMap = new Map(
|
||||
unSanitizedEditorConfig.features.map((f) => [f.key, f] as [string, FeatureProvider]),
|
||||
)
|
||||
|
||||
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
|
||||
let loaded = 0
|
||||
for (const featureProvider of unSanitizedEditorConfig.features) {
|
||||
if (!featureProvider.key) {
|
||||
throw new Error(
|
||||
@@ -163,12 +174,14 @@ export function loadFeatures({
|
||||
})
|
||||
resolvedFeatures.set(featureProvider.key, {
|
||||
...feature,
|
||||
Component: featureProvider.Component,
|
||||
dependencies: featureProvider.dependencies,
|
||||
dependenciesPriority: featureProvider.dependenciesPriority,
|
||||
dependenciesSoft: featureProvider.dependenciesSoft,
|
||||
key: featureProvider.key,
|
||||
order: loaded,
|
||||
})
|
||||
|
||||
loaded++
|
||||
}
|
||||
|
||||
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 { FeatureProvider, ResolvedFeatureMap, SanitizedFeatures } from '../../features/types'
|
||||
import type {
|
||||
FeatureProviderClient,
|
||||
FeatureProviderServer,
|
||||
ResolvedClientFeatureMap,
|
||||
ResolvedServerFeatureMap,
|
||||
SanitizedClientFeatures,
|
||||
SanitizedServerFeatures,
|
||||
} from '../../features/types'
|
||||
|
||||
export type EditorConfig = {
|
||||
features: FeatureProvider[]
|
||||
lexical?: () => Promise<LexicalEditorConfig>
|
||||
export type ServerEditorConfig = {
|
||||
features: FeatureProviderServer<unknown, unknown>[]
|
||||
lexical?: LexicalEditorConfig
|
||||
}
|
||||
|
||||
export type SanitizedEditorConfig = {
|
||||
features: SanitizedFeatures
|
||||
lexical: () => Promise<LexicalEditorConfig>
|
||||
resolvedFeatureMap: ResolvedFeatureMap
|
||||
export type SanitizedServerEditorConfig = {
|
||||
features: SanitizedServerFeatures
|
||||
lexical: LexicalEditorConfig
|
||||
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 { LexicalNodeReplacement } from 'lexical'
|
||||
|
||||
import type { SanitizedEditorConfig } from '../config/types'
|
||||
import type { SanitizedClientEditorConfig, SanitizedServerEditorConfig } from '../config/types'
|
||||
|
||||
export function getEnabledNodes({
|
||||
editorConfig,
|
||||
}: {
|
||||
editorConfig: SanitizedEditorConfig
|
||||
editorConfig: SanitizedClientEditorConfig | SanitizedServerEditorConfig
|
||||
}): 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 { $getSelection } from 'lexical'
|
||||
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 type { FloatingToolbarSectionEntry } from '../types'
|
||||
@@ -24,7 +24,7 @@ export function DropDownItem({
|
||||
children: React.ReactNode
|
||||
entry: FloatingToolbarSectionEntry
|
||||
title?: string
|
||||
}): JSX.Element {
|
||||
}): React.ReactNode {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [enabled, setEnabled] = useState<boolean>(true)
|
||||
const [active, setActive] = useState<boolean>(false)
|
||||
@@ -209,7 +209,7 @@ export function DropDown({
|
||||
children: ReactNode
|
||||
disabled?: boolean
|
||||
stopCloseOnClickSelf?: boolean
|
||||
}): JSX.Element {
|
||||
}): React.ReactNode {
|
||||
const dropDownRef = useRef<HTMLDivElement>(null)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const [showDropDown, setShowDropDown] = useState(false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '@payloadcms/ui/scss';
|
||||
@import '../../../../../../../ui/src/scss/styles.scss';
|
||||
|
||||
.floating-select-toolbar-popup__dropdown {
|
||||
display: flex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React, { useMemo } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
const baseClass = 'floating-select-toolbar-popup__dropdown'
|
||||
|
||||
@@ -19,43 +19,17 @@ export const ToolbarEntry = ({
|
||||
editor: LexicalEditor
|
||||
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) {
|
||||
return (
|
||||
Component && (
|
||||
<React.Suspense>
|
||||
<Component anchorElem={anchorElem} editor={editor} entry={entry} key={entry.key} />
|
||||
</React.Suspense>
|
||||
entry?.Component && (
|
||||
<entry.Component anchorElem={anchorElem} editor={editor} entry={entry} key={entry.key} />
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropDownItem entry={entry} key={entry.key}>
|
||||
{ChildComponent && (
|
||||
<React.Suspense>
|
||||
<ChildComponent />
|
||||
</React.Suspense>
|
||||
)}
|
||||
{entry?.ChildComponent && <entry.ChildComponent />}
|
||||
<span className="text">{entry.label}</span>
|
||||
</DropDownItem>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '@payloadcms/ui/scss';
|
||||
@import '../../../../../../ui/src/scss/styles.scss';
|
||||
|
||||
html[data-theme='light'] {
|
||||
.floating-select-toolbar-popup {
|
||||
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
COMMAND_PRIORITY_LOW,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
} from 'lexical'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import * as React from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
import type { FloatingToolbarSection, FloatingToolbarSectionEntry } from './types'
|
||||
|
||||
import { useEditorConfigContext } from '../../config/EditorConfigProvider'
|
||||
import { useEditorConfigContext } from '../../config/client/EditorConfigProvider'
|
||||
import { getDOMRangeRect } from '../../utils/getDOMRangeRect'
|
||||
import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition'
|
||||
import { ToolbarButton } from './ToolbarButton'
|
||||
@@ -31,44 +31,18 @@ function ButtonSectionEntry({
|
||||
anchorElem: HTMLElement
|
||||
editor: LexicalEditor
|
||||
entry: FloatingToolbarSectionEntry
|
||||
}): JSX.Element {
|
||||
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])
|
||||
|
||||
}): React.ReactNode {
|
||||
if (entry.Component) {
|
||||
return (
|
||||
Component && (
|
||||
<React.Suspense>
|
||||
<Component anchorElem={anchorElem} editor={editor} entry={entry} key={entry.key} />{' '}
|
||||
</React.Suspense>
|
||||
entry?.Component && (
|
||||
<entry.Component anchorElem={anchorElem} editor={editor} entry={entry} key={entry.key} />
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarButton entry={entry} key={entry.key}>
|
||||
{ChildComponent && (
|
||||
<React.Suspense>
|
||||
<ChildComponent />
|
||||
</React.Suspense>
|
||||
)}
|
||||
{entry?.ChildComponent && <entry.ChildComponent />}
|
||||
</ToolbarButton>
|
||||
)
|
||||
}
|
||||
@@ -83,18 +57,13 @@ function ToolbarSection({
|
||||
editor: LexicalEditor
|
||||
index: number
|
||||
section: FloatingToolbarSection
|
||||
}): JSX.Element {
|
||||
}): React.ReactNode {
|
||||
const { editorConfig } = useEditorConfigContext()
|
||||
|
||||
const Icon = useMemo(() => {
|
||||
return section?.type === 'dropdown' && section.entries.length && section.ChildComponent
|
||||
? React.lazy(() =>
|
||||
section.ChildComponent().then((resolvedComponent) => ({
|
||||
default: resolvedComponent,
|
||||
})),
|
||||
)
|
||||
const Icon =
|
||||
section?.type === 'dropdown' && section.entries.length && section.ChildComponent
|
||||
? section.ChildComponent
|
||||
: null
|
||||
}, [section])
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -104,15 +73,13 @@ function ToolbarSection({
|
||||
{section.type === 'dropdown' &&
|
||||
section.entries.length &&
|
||||
(Icon ? (
|
||||
<React.Suspense>
|
||||
<ToolbarDropdown
|
||||
Icon={Icon}
|
||||
anchorElem={anchorElem}
|
||||
editor={editor}
|
||||
entries={section.entries}
|
||||
sectionKey={section.key}
|
||||
/>
|
||||
</React.Suspense>
|
||||
<ToolbarDropdown
|
||||
Icon={Icon}
|
||||
anchorElem={anchorElem}
|
||||
editor={editor}
|
||||
entries={section.entries}
|
||||
sectionKey={section.key}
|
||||
/>
|
||||
) : (
|
||||
<ToolbarDropdown
|
||||
anchorElem={anchorElem}
|
||||
@@ -146,7 +113,7 @@ function FloatingSelectToolbar({
|
||||
}: {
|
||||
anchorElem: HTMLElement
|
||||
editor: LexicalEditor
|
||||
}): JSX.Element {
|
||||
}): React.ReactNode {
|
||||
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null)
|
||||
const caretRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type React from 'react'
|
||||
|
||||
export type FloatingToolbarSection =
|
||||
| {
|
||||
ChildComponent?: () => Promise<React.FC>
|
||||
ChildComponent?: React.FC
|
||||
entries: Array<FloatingToolbarSectionEntry>
|
||||
key: string
|
||||
order?: number
|
||||
@@ -17,15 +17,13 @@ export type FloatingToolbarSection =
|
||||
}
|
||||
|
||||
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 */
|
||||
Component?: () => Promise<
|
||||
React.FC<{
|
||||
anchorElem: HTMLElement
|
||||
editor: LexicalEditor
|
||||
entry: FloatingToolbarSectionEntry
|
||||
}>
|
||||
>
|
||||
Component?: React.FC<{
|
||||
anchorElem: HTMLElement
|
||||
editor: LexicalEditor
|
||||
entry: FloatingToolbarSectionEntry
|
||||
}>
|
||||
isActive?: ({ editor, selection }: { editor: LexicalEditor; selection: BaseSelection }) => boolean
|
||||
isEnabled?: ({
|
||||
editor,
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import { MarkdownShortcutPlugin as LexicalMarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin'
|
||||
import * as React from 'react'
|
||||
|
||||
import { useEditorConfigContext } from '../../config/EditorConfigProvider'
|
||||
import { useEditorConfigContext } from '../../config/client/EditorConfigProvider'
|
||||
|
||||
export const MarkdownShortcutPlugin: React.FC = () => {
|
||||
const { editorConfig } = useEditorConfigContext()
|
||||
|
||||
console.log('traaaaa', editorConfig.features.markdownTransformers)
|
||||
return <LexicalMarkdownShortcutPlugin transformers={editorConfig.features.markdownTransformers} />
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type React from 'react'
|
||||
|
||||
export class SlashMenuOption {
|
||||
// Icon for display
|
||||
Icon: () => Promise<React.FC>
|
||||
Icon: React.FC
|
||||
|
||||
displayName?: (({ i18n }: { i18n: I18n }) => string) | string
|
||||
// Used for class names and, if displayName is not provided, for display.
|
||||
@@ -22,7 +22,7 @@ export class SlashMenuOption {
|
||||
constructor(
|
||||
key: string,
|
||||
options: {
|
||||
Icon: () => Promise<React.FC>
|
||||
Icon: React.FC
|
||||
displayName?: (({ i18n }: { i18n: I18n }) => string) | string
|
||||
keyboardShortcut?: string
|
||||
keywords?: Array<string>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '@payloadcms/ui/scss';
|
||||
@import '../../../../../../ui/src/scss/styles.scss';
|
||||
|
||||
html[data-theme='light'] {
|
||||
.slash-menu-popup {
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as ReactDOM from 'react-dom'
|
||||
|
||||
import type { SlashMenuGroup, SlashMenuOption } from './LexicalTypeaheadMenuPlugin/types'
|
||||
|
||||
import { useEditorConfigContext } from '../../config/EditorConfigProvider'
|
||||
import { useEditorConfigContext } from '../../config/client/EditorConfigProvider'
|
||||
import { LexicalTypeaheadMenuPlugin } from './LexicalTypeaheadMenuPlugin'
|
||||
import './index.scss'
|
||||
import { useMenuTriggerMatch } from './useMenuTriggerMatch'
|
||||
@@ -45,16 +45,6 @@ function SlashMenuItem({
|
||||
title = title.substring(0, 25) + '...'
|
||||
}
|
||||
|
||||
const LazyIcon = useMemo(() => {
|
||||
return option?.Icon
|
||||
? React.lazy(() =>
|
||||
option.Icon().then((resolvedIcon) => ({
|
||||
default: resolvedIcon,
|
||||
})),
|
||||
)
|
||||
: null
|
||||
}, [option])
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-selected={isSelected}
|
||||
@@ -68,11 +58,7 @@ function SlashMenuItem({
|
||||
tabIndex={-1}
|
||||
type="button"
|
||||
>
|
||||
{LazyIcon && (
|
||||
<React.Suspense>
|
||||
<LazyIcon />
|
||||
</React.Suspense>
|
||||
)}
|
||||
{option?.Icon && <option.Icon />}
|
||||
|
||||
<span className={`${baseClass}__item-text`}>{title}</span>
|
||||
</button>
|
||||
|
||||
@@ -1,23 +1,104 @@
|
||||
import type { RichTextAdapter } from 'payload/types'
|
||||
|
||||
import { mapFields } from '@payloadcms/ui/utilities'
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
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 =
|
||||
(args: { resolvedFeatureMap: ResolvedFeatureMap }): RichTextAdapter['generateComponentMap'] =>
|
||||
({ config }) => {
|
||||
(args: {
|
||||
resolvedFeatureMap: ResolvedServerFeatureMap
|
||||
}): RichTextAdapter['generateComponentMap'] =>
|
||||
({ config, schemaPath }) => {
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
|
||||
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()) {
|
||||
console.log('key', key)
|
||||
const resolvedFeature = args.resolvedFeatureMap.get(key)
|
||||
const Component = resolvedFeature.Component
|
||||
componentMap.set(`feature.${key}`, <Component />)
|
||||
}
|
||||
componentMap.set(
|
||||
`features`,
|
||||
resolvedFeatureMapArray.map(([featureKey, resolvedFeature]) => {
|
||||
const ClientComponent = resolvedFeature.ClientComponent
|
||||
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 />)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}),
|
||||
)
|
||||
|
||||
console.log('componentMaaap', 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 type { FeatureProvider, ResolvedFeatureMap } from './field/features/types'
|
||||
import type { EditorConfig, SanitizedEditorConfig } from './field/lexical/config/types'
|
||||
import type { FeatureProviderServer, ResolvedServerFeatureMap } from './field/features/types'
|
||||
import type { SanitizedServerEditorConfig } from './field/lexical/config/types'
|
||||
import type { AdapterProps } from './types'
|
||||
|
||||
import {
|
||||
defaultEditorConfig,
|
||||
defaultEditorFeatures,
|
||||
defaultSanitizedEditorConfig,
|
||||
} from './field/lexical/config/default'
|
||||
import { loadFeatures } from './field/lexical/config/loader'
|
||||
import { sanitizeFeatures } from './field/lexical/config/sanitize'
|
||||
defaultSanitizedServerEditorConfig,
|
||||
} from './field/lexical/config/server/default'
|
||||
import { loadFeatures } from './field/lexical/config/server/loader'
|
||||
import { sanitizeServerFeatures } from './field/lexical/config/server/sanitize'
|
||||
import { cloneDeep } from './field/lexical/utils/cloneDeep'
|
||||
import { getGenerateComponentMap } from './generateComponentMap'
|
||||
import { getGenerateSchemaMap } from './generateSchemaMap'
|
||||
import { richTextRelationshipPromise } from './populate/richTextRelationshipPromise'
|
||||
import { richTextValidateHOC } from './validate'
|
||||
|
||||
export type LexicalEditorProps = {
|
||||
features?:
|
||||
| (({ defaultFeatures }: { defaultFeatures: FeatureProvider[] }) => FeatureProvider[])
|
||||
| FeatureProvider[]
|
||||
| (({
|
||||
defaultFeatures,
|
||||
}: {
|
||||
defaultFeatures: FeatureProviderServer<unknown, unknown>[]
|
||||
}) => FeatureProviderServer<unknown, unknown>[])
|
||||
| FeatureProviderServer<unknown, unknown>[]
|
||||
lexical?: LexicalEditorConfig
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
export type LexicalRichTextAdapter = RichTextAdapter<SerializedEditorState, AdapterProps, any> & {
|
||||
editorConfig: SanitizedEditorConfig
|
||||
editorConfig: SanitizedServerEditorConfig
|
||||
}
|
||||
|
||||
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)) {
|
||||
finalSanitizedEditorConfig = cloneDeep(defaultSanitizedEditorConfig)
|
||||
finalSanitizedEditorConfig = cloneDeep(defaultSanitizedServerEditorConfig)
|
||||
resolvedFeatureMap = finalSanitizedEditorConfig.resolvedFeatureMap
|
||||
} else {
|
||||
let features: FeatureProvider[] =
|
||||
let features: FeatureProviderServer<unknown, unknown>[] =
|
||||
props.features && typeof props.features === 'function'
|
||||
? props.features({ defaultFeatures: cloneDeep(defaultEditorFeatures) })
|
||||
: (props.features as FeatureProvider[])
|
||||
: (props.features as FeatureProviderServer<unknown, unknown>[])
|
||||
if (!features) {
|
||||
features = cloneDeep(defaultEditorFeatures)
|
||||
}
|
||||
@@ -53,14 +59,14 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
resolvedFeatureMap = loadFeatures({
|
||||
unSanitizedEditorConfig: {
|
||||
features,
|
||||
lexical: lexical ? () => Promise.resolve(lexical) : defaultEditorConfig.lexical,
|
||||
lexical: lexical ? lexical : defaultEditorConfig.lexical,
|
||||
},
|
||||
})
|
||||
|
||||
finalSanitizedEditorConfig = {
|
||||
features: sanitizeFeatures(resolvedFeatureMap),
|
||||
lexical: lexical ? () => Promise.resolve(lexical) : defaultEditorConfig.lexical,
|
||||
resolvedFeatureMap: resolvedFeatureMap,
|
||||
features: sanitizeServerFeatures(resolvedFeatureMap),
|
||||
lexical: lexical ? lexical : defaultEditorConfig.lexical,
|
||||
resolvedFeatureMap,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +119,10 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
},
|
||||
editorConfig: finalSanitizedEditorConfig,
|
||||
generateComponentMap: getGenerateComponentMap({
|
||||
resolvedFeatureMap: resolvedFeatureMap,
|
||||
resolvedFeatureMap,
|
||||
}),
|
||||
generateSchemaMap: getGenerateSchemaMap({
|
||||
resolvedFeatureMap,
|
||||
}),
|
||||
outputSchema: ({ field, interfaceNameDefinitions, isRequired }) => {
|
||||
let outputSchema: JSONSchema4 = {
|
||||
@@ -234,23 +243,6 @@ export {
|
||||
} from './field/features/Blocks/nodes/BlocksNode'
|
||||
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 { RelationshipFeature } from './field/features/Relationship'
|
||||
export {
|
||||
@@ -260,6 +252,7 @@ export {
|
||||
RelationshipNode,
|
||||
type SerializedRelationshipNode,
|
||||
} from './field/features/Relationship/nodes/RelationshipNode'
|
||||
|
||||
export { UploadFeature } from './field/features/Upload'
|
||||
export type { UploadFeatureProps } from './field/features/Upload'
|
||||
export type { RawUploadPayload } from './field/features/Upload/nodes/UploadNode'
|
||||
@@ -270,7 +263,7 @@ export {
|
||||
type UploadData,
|
||||
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 {
|
||||
HTMLConverterFeature,
|
||||
@@ -287,19 +280,36 @@ export { defaultHTMLConverters } from './field/features/converters/html/converte
|
||||
export type { HTMLConverter } from './field/features/converters/html/converter/types'
|
||||
export { consolidateHTMLConverters } from './field/features/converters/html/field'
|
||||
export { lexicalHTML } from './field/features/converters/html/field'
|
||||
|
||||
export { TestRecorderFeature } from './field/features/debug/TestRecorder'
|
||||
export { TreeViewFeature } from './field/features/debug/TreeView'
|
||||
|
||||
export { BoldTextFeature } from './field/features/format/Bold'
|
||||
|
||||
export { InlineCodeTextFeature } from './field/features/format/InlineCode'
|
||||
export { ItalicTextFeature } from './field/features/format/Italic'
|
||||
|
||||
export { SectionWithEntries as FormatSectionWithEntries } from './field/features/format/common/floatingSelectToolbarSection'
|
||||
export { StrikethroughTextFeature } from './field/features/format/strikethrough'
|
||||
export { SubscriptTextFeature } from './field/features/format/subscript'
|
||||
export { SuperscriptTextFeature } from './field/features/format/superscript'
|
||||
export { UnderlineTextFeature } from './field/features/format/underline'
|
||||
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 { OrderedListFeature } from './field/features/lists/OrderedList'
|
||||
export { UnorderedListFeature } from './field/features/lists/UnorderedList'
|
||||
@@ -330,26 +340,50 @@ export type {
|
||||
} from './field/features/migrations/SlateToLexical/converter/types'
|
||||
|
||||
export type {
|
||||
Feature,
|
||||
FeatureProvider,
|
||||
FeatureProviderMap,
|
||||
ClientFeature,
|
||||
ClientFeatureProviderMap,
|
||||
FeatureProviderClient,
|
||||
FeatureProviderProviderClient,
|
||||
FeatureProviderProviderServer,
|
||||
FeatureProviderServer,
|
||||
NodeValidation,
|
||||
PopulationPromise,
|
||||
ResolvedFeature,
|
||||
ResolvedFeatureMap,
|
||||
SanitizedFeatures,
|
||||
ResolvedClientFeature,
|
||||
ResolvedClientFeatureMap,
|
||||
ResolvedServerFeature,
|
||||
ResolvedServerFeatureMap,
|
||||
SanitizedClientFeatures,
|
||||
SanitizedPlugin,
|
||||
SanitizedServerFeatures,
|
||||
ServerFeature,
|
||||
ServerFeatureProviderMap,
|
||||
} from './field/features/types'
|
||||
export {
|
||||
EditorConfigProvider,
|
||||
useEditorConfigContext,
|
||||
} from './field/lexical/config/EditorConfigProvider'
|
||||
} from './field/lexical/config/client/EditorConfigProvider'
|
||||
export {
|
||||
sanitizeClientEditorConfig,
|
||||
sanitizeClientFeatures,
|
||||
} from './field/lexical/config/client/sanitize'
|
||||
export {
|
||||
defaultEditorConfig,
|
||||
defaultEditorFeatures,
|
||||
defaultSanitizedEditorConfig,
|
||||
} from './field/lexical/config/default'
|
||||
export { loadFeatures, sortFeaturesForOptimalLoading } from './field/lexical/config/loader'
|
||||
export { sanitizeEditorConfig, sanitizeFeatures } from './field/lexical/config/sanitize'
|
||||
defaultEditorLexicalConfig,
|
||||
defaultSanitizedServerEditorConfig,
|
||||
} from './field/lexical/config/server/default'
|
||||
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 {
|
||||
@@ -357,8 +391,6 @@ export {
|
||||
type FloatingToolbarSectionEntry,
|
||||
} from './field/lexical/plugins/FloatingSelectToolbar/types'
|
||||
export { ENABLE_SLASH_MENU_COMMAND } from './field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/index'
|
||||
// export SanitizedEditorConfig
|
||||
export type { EditorConfig, SanitizedEditorConfig }
|
||||
export type { AdapterProps }
|
||||
|
||||
export {
|
||||
|
||||
@@ -2,8 +2,9 @@ import type { SerializedEditorState } from 'lexical'
|
||||
import type { FieldPermissions } from 'payload/auth'
|
||||
import type { FieldTypes } from 'payload/config'
|
||||
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> & {
|
||||
fieldTypes: FieldTypes
|
||||
@@ -13,5 +14,11 @@ export type FieldProps = RichTextFieldProps<SerializedEditorState, 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'
|
||||
|
||||
type FeatureProviderGetter = () => FeatureProvider
|
||||
|
||||
export const useLexicalFeature = (key: string, feature: FeatureProviderGetter) => {
|
||||
export const useLexicalFeature = (key: string, feature: FeatureProvider) => {
|
||||
const { schemaPath } = useFieldPath()
|
||||
|
||||
useAddClientFunction(`lexicalFeature.${schemaPath}.${key}`, feature)
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"src/**/*.spec.tsx"
|
||||
],
|
||||
"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 { richTextComponentMap } = fieldProps
|
||||
const fieldMap = richTextComponentMap.get(fieldMapPath)
|
||||
const fieldMap = richTextComponentMap.get(linkFieldsSchemaPath)
|
||||
|
||||
const editor = useSlate()
|
||||
const config = useConfig()
|
||||
@@ -113,7 +113,7 @@ export const LinkElement = () => {
|
||||
setInitialState(state)
|
||||
}
|
||||
|
||||
awaitInitialState()
|
||||
void awaitInitialState()
|
||||
}, [renderModal, element, user, locale, t, getDocPreferences, config, id, fieldMapPath])
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,20 +5,22 @@ import type { Editor } from 'slate'
|
||||
|
||||
import { useSlatePlugin } from '../../../utilities/useSlatePlugin'
|
||||
|
||||
export const WithLinks: React.FC = () => {
|
||||
useSlatePlugin('withLinks', (incomingEditor: Editor): Editor => {
|
||||
const editor = incomingEditor
|
||||
const { isInline } = editor
|
||||
const plugin = (incomingEditor: Editor): Editor => {
|
||||
const editor = incomingEditor
|
||||
const { isInline } = editor
|
||||
|
||||
editor.isInline = (element) => {
|
||||
if (element.type === 'link') {
|
||||
return true
|
||||
}
|
||||
|
||||
return isInline(element)
|
||||
editor.isInline = (element) => {
|
||||
if (element.type === 'link') {
|
||||
return true
|
||||
}
|
||||
|
||||
return editor
|
||||
})
|
||||
return isInline(element)
|
||||
}
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
export const WithLinks: React.FC = () => {
|
||||
useSlatePlugin('withLinks', plugin)
|
||||
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 FieldDescription } from '../forms/FieldDescription'
|
||||
export { FieldPathProvider } from '../forms/FieldPathProvider'
|
||||
export { useFieldPath } from '../forms/FieldPathProvider'
|
||||
export { default as Form } from '../forms/Form'
|
||||
export { default as buildInitialState } from '../forms/Form'
|
||||
export {
|
||||
|
||||
@@ -37,7 +37,7 @@ export const useAddClientFunction = (key: string, func: any) => {
|
||||
func,
|
||||
key,
|
||||
})
|
||||
}, [func, key])
|
||||
}, [func, key, addClientFunction])
|
||||
}
|
||||
|
||||
export const useClientFunctions = () => {
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -1205,6 +1205,9 @@ importers:
|
||||
'@types/react':
|
||||
specifier: 18.2.15
|
||||
version: 18.2.15
|
||||
'@types/react-dom':
|
||||
specifier: 18.2.7
|
||||
version: 18.2.7
|
||||
payload:
|
||||
specifier: workspace:*
|
||||
version: link:../payload
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlignFeature, LinkFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import path from 'path'
|
||||
|
||||
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 { postgresAdapter } from '../packages/db-postgres/src'
|
||||
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'
|
||||
|
||||
@@ -55,7 +56,7 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
|
||||
const config: Config = {
|
||||
db: databaseAdapters[process.env.PAYLOAD_DATABASE || 'mongoose'],
|
||||
secret: 'TEST_SECRET',
|
||||
editor: slateEditor({
|
||||
/*editor: slateEditor({
|
||||
admin: {
|
||||
upload: {
|
||||
collections: {
|
||||
@@ -70,6 +71,9 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
|
||||
},
|
||||
},
|
||||
},
|
||||
}),*/
|
||||
editor: lexicalEditor({
|
||||
features: [LinkFeature({}), AlignFeature()],
|
||||
}),
|
||||
rateLimit: {
|
||||
max: 9999999999,
|
||||
|
||||
Reference in New Issue
Block a user