feat(richtext-lexical)!: lazy import React components to prevent client-only code from leaking into the server (#4290)
* chore(richtext-lexical): lazy import all React things * chore(richtext-lexical): use useMemo for lazy-loaded React Components to prevent lag and flashes when parent component re-renders * chore: make exportPointerFiles.ts script usable for other packages as well by hoisting it up to the workspace root and making it configurable * chore(richtext-lexical): make sure no client-side code is imported by default from Features * chore(richtext-lexical): remove unnecessary scss files * chore(richtext-lexical): adjust package.json exports * chore(richtext-*): lazy-import Field & Cell Components, move Client-only exports to /components subpath export * chore(richtext-lexical): make sure nothing client-side is directly exported from the / subpath export anymore * add missing imports * chore: remove breaking changes for Slate * LazyCellComponent & LazyFieldComponent
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && pnpm build:components && ts-node -T ./scripts/exportPointerFiles.ts",
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && pnpm build:components && ts-node -T ../../scripts/exportPointerFiles.ts ../packages/payload dist/exports",
|
||||
"build:components": "webpack --config dist/admin/components.config.js",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
|
||||
@@ -1,13 +1,34 @@
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import type { RichTextField } from '../../../../../fields/config/types'
|
||||
import type { RichTextAdapter } from './types'
|
||||
const RichText: React.FC<RichTextField> = (fieldprops) => {
|
||||
// eslint-disable-next-line react/destructuring-assignment
|
||||
const editor: RichTextAdapter = fieldprops.editor
|
||||
const { FieldComponent } = editor
|
||||
|
||||
return <FieldComponent {...fieldprops} />
|
||||
const isLazy = 'LazyFieldComponent' in editor
|
||||
|
||||
const ImportedFieldComponent: React.FC<any> = useMemo(() => {
|
||||
return isLazy
|
||||
? React.lazy(() => {
|
||||
return editor.LazyFieldComponent().then((resolvedComponent) => ({
|
||||
default: resolvedComponent,
|
||||
}))
|
||||
})
|
||||
: null
|
||||
}, [editor, isLazy])
|
||||
|
||||
if (isLazy) {
|
||||
return (
|
||||
ImportedFieldComponent && (
|
||||
<React.Suspense>
|
||||
<ImportedFieldComponent {...fieldprops} />
|
||||
</React.Suspense>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return <editor.FieldComponent {...fieldprops} />
|
||||
}
|
||||
|
||||
export default RichText
|
||||
|
||||
@@ -13,15 +13,11 @@ export type RichTextFieldProps<
|
||||
path?: string
|
||||
}
|
||||
|
||||
export type RichTextAdapter<
|
||||
type RichTextAdapterBase<
|
||||
Value extends object = object,
|
||||
AdapterProps = any,
|
||||
ExtraFieldProperties = {},
|
||||
> = {
|
||||
CellComponent: React.FC<
|
||||
CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>
|
||||
>
|
||||
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
|
||||
afterReadPromise?: ({
|
||||
field,
|
||||
incomingEditorState,
|
||||
@@ -31,7 +27,6 @@ export type RichTextAdapter<
|
||||
incomingEditorState: Value
|
||||
siblingDoc: Record<string, unknown>
|
||||
}) => Promise<void> | null
|
||||
|
||||
outputSchema?: ({
|
||||
field,
|
||||
isRequired,
|
||||
@@ -59,3 +54,25 @@ export type RichTextAdapter<
|
||||
RichTextField<Value, AdapterProps, ExtraFieldProperties>
|
||||
>
|
||||
}
|
||||
|
||||
export type RichTextAdapter<
|
||||
Value extends object = object,
|
||||
AdapterProps = any,
|
||||
ExtraFieldProperties = {},
|
||||
> = RichTextAdapterBase<Value, AdapterProps, ExtraFieldProperties> &
|
||||
(
|
||||
| {
|
||||
CellComponent: React.FC<
|
||||
CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>
|
||||
>
|
||||
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
|
||||
}
|
||||
| {
|
||||
LazyCellComponent: () => Promise<
|
||||
React.FC<CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>>
|
||||
>
|
||||
LazyFieldComponent: () => Promise<
|
||||
React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
|
||||
>
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import type { RichTextField } from '../../../../../../../../fields/config/types'
|
||||
import type { RichTextAdapter } from '../../../../../../forms/field-types/RichText/types'
|
||||
@@ -7,9 +7,30 @@ import type { CellComponentProps } from '../../types'
|
||||
const RichTextCell: React.FC<CellComponentProps<RichTextField>> = (props) => {
|
||||
// eslint-disable-next-line react/destructuring-assignment
|
||||
const editor: RichTextAdapter = props.field.editor
|
||||
const { CellComponent } = editor
|
||||
|
||||
return <CellComponent {...props} />
|
||||
const isLazy = 'LazyCellComponent' in editor
|
||||
|
||||
const ImportedCellComponent: React.FC<any> = useMemo(() => {
|
||||
return isLazy
|
||||
? React.lazy(() => {
|
||||
return editor.LazyCellComponent().then((resolvedComponent) => ({
|
||||
default: resolvedComponent,
|
||||
}))
|
||||
})
|
||||
: null
|
||||
}, [editor, isLazy])
|
||||
|
||||
if (isLazy) {
|
||||
return (
|
||||
ImportedCellComponent && (
|
||||
<React.Suspense>
|
||||
<ImportedCellComponent {...props} />
|
||||
</React.Suspense>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return <editor.CellComponent {...props} />
|
||||
}
|
||||
|
||||
export default RichTextCell
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import joi from 'joi'
|
||||
|
||||
import { adminViewSchema } from './shared/adminViewSchema'
|
||||
import { livePreviewSchema } from './shared/componentSchema'
|
||||
import { componentSchema, livePreviewSchema } from './shared/componentSchema'
|
||||
|
||||
const component = joi.alternatives().try(joi.object().unknown(), joi.func())
|
||||
|
||||
@@ -94,8 +94,10 @@ export default joi.object({
|
||||
.object()
|
||||
.required()
|
||||
.keys({
|
||||
CellComponent: component.required(),
|
||||
FieldComponent: component.required(),
|
||||
CellComponent: componentSchema.optional(),
|
||||
FieldComponent: componentSchema.optional(),
|
||||
LazyCellComponent: joi.func().optional(),
|
||||
LazyFieldComponent: joi.func().optional(),
|
||||
afterReadPromise: joi.func().optional(),
|
||||
outputSchema: joi.func().optional(),
|
||||
populationPromise: joi.func().optional(),
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
Important:
|
||||
|
||||
When you export anything with a scss or svg, or any component with a hook, it should be exported from a file within payload/components
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -436,8 +436,10 @@ export const richText = baseField.keys({
|
||||
editor: joi
|
||||
.object()
|
||||
.keys({
|
||||
CellComponent: componentSchema.required(),
|
||||
FieldComponent: componentSchema.required(),
|
||||
CellComponent: componentSchema.optional(),
|
||||
FieldComponent: componentSchema.optional(),
|
||||
LazyCellComponent: joi.func().optional(),
|
||||
LazyFieldComponent: joi.func().optional(),
|
||||
afterReadPromise: joi.func().optional(),
|
||||
outputSchema: joi.func().optional(),
|
||||
populationPromise: joi.func().optional(),
|
||||
|
||||
4
packages/richtext-lexical/.gitignore
vendored
Normal file
4
packages/richtext-lexical/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/utilities.d.ts
|
||||
/utilities.js
|
||||
/components.d.ts
|
||||
/components.js
|
||||
@@ -9,7 +9,7 @@
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && ts-node -T ../../scripts/exportPointerFiles.ts ../packages/richtext-lexical dist/exports",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build",
|
||||
@@ -51,8 +51,14 @@
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"default": "./src/index.ts",
|
||||
"import": "./src/index.ts",
|
||||
"require": "./src/index.ts",
|
||||
"types": "./src/index.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./src/exports/*.ts",
|
||||
"require": "./src/exports/*.ts",
|
||||
"types": "./src/exports/*.ts"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
@@ -62,6 +68,8 @@
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist",
|
||||
"components.js",
|
||||
"components.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||
import type { CellComponentProps, RichTextField } from 'payload/types'
|
||||
|
||||
import { createHeadlessEditor } from '@lexical/headless'
|
||||
@@ -50,11 +51,12 @@ export const RichTextCell: React.FC<
|
||||
return
|
||||
}
|
||||
|
||||
editorConfig.lexical().then((lexicalConfig: LexicalEditorConfig) => {
|
||||
// initialize headless editor
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
namespace: editorConfig.lexical.namespace,
|
||||
namespace: lexicalConfig.namespace,
|
||||
nodes: getEnabledNodes({ editorConfig }),
|
||||
theme: editorConfig.lexical.theme,
|
||||
theme: lexicalConfig.theme,
|
||||
})
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse))
|
||||
|
||||
@@ -65,6 +67,7 @@ export const RichTextCell: React.FC<
|
||||
|
||||
// Limiting the number of characters shown is done in a CSS rule
|
||||
setPreview(textContent)
|
||||
})
|
||||
}, [data, editorConfig])
|
||||
|
||||
return <span>{preview}</span>
|
||||
|
||||
6
packages/richtext-lexical/src/exports/components.ts
Normal file
6
packages/richtext-lexical/src/exports/components.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { RichTextCell } from '../cell'
|
||||
export { RichTextField } from '../field'
|
||||
|
||||
export { defaultEditorLexicalConfig } from '../field/lexical/config/defaultClient'
|
||||
export { ToolbarButton } from '../field/lexical/plugins/FloatingSelectToolbar/ToolbarButton'
|
||||
export { ToolbarDropdown } from '../field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/index'
|
||||
@@ -73,11 +73,11 @@ const RichText: React.FC<FieldProps> = (props) => {
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
|
||||
<ErrorBoundary fallbackRender={fallbackRender} onReset={(details) => {}}>
|
||||
<ErrorBoundary fallbackRender={fallbackRender} onReset={() => {}}>
|
||||
<LexicalProvider
|
||||
editorConfig={editorConfig}
|
||||
fieldProps={props}
|
||||
onChange={(editorState, editor, tags) => {
|
||||
onChange={(editorState) => {
|
||||
let serializedEditorState = editorState.toJSON()
|
||||
|
||||
// Transform state through save hooks
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { HTMLConverter } from '../converters/html/converter/types'
|
||||
import type { FeatureProvider } from '../types'
|
||||
|
||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||
import { BlockquoteIcon } from '../../lexical/ui/icons/Blockquote'
|
||||
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
|
||||
import { convertLexicalNodesToHTML } from '../converters/html/converter'
|
||||
import { MarkdownTransformer } from './markdownTransformer'
|
||||
@@ -21,8 +20,12 @@ export const BlockQuoteFeature = (): FeatureProvider => {
|
||||
sections: [
|
||||
TextDropdownSectionWithEntries([
|
||||
{
|
||||
ChildComponent: BlockquoteIcon,
|
||||
isActive: ({ editor, selection }) => false,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/Blockquote').then(
|
||||
(module) => module.BlockquoteIcon,
|
||||
),
|
||||
isActive: () => false,
|
||||
key: 'blockquote',
|
||||
label: `Blockquote`,
|
||||
onClick: ({ editor }) => {
|
||||
@@ -70,7 +73,11 @@ export const BlockQuoteFeature = (): FeatureProvider => {
|
||||
key: 'basic',
|
||||
options: [
|
||||
new SlashMenuOption(`blockquote`, {
|
||||
Icon: BlockquoteIcon,
|
||||
Icon: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/Blockquote').then(
|
||||
(module) => module.BlockquoteIcon,
|
||||
),
|
||||
displayName: `Blockquote`,
|
||||
keywords: ['quote', 'blockquote'],
|
||||
onSelect: () => {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import 'payload/scss';
|
||||
@@ -18,8 +18,7 @@ import type { BlocksFeatureProps } from '..'
|
||||
|
||||
import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider'
|
||||
import { $createBlockNode } from '../nodes/BlocksNode'
|
||||
import { INSERT_BLOCK_COMMAND } from '../plugin'
|
||||
import './index.scss'
|
||||
import { INSERT_BLOCK_COMMAND } from '../plugin/commands'
|
||||
const baseClass = 'lexical-blocks-drawer'
|
||||
|
||||
export const INSERT_BLOCK_WITH_DRAWER_COMMAND: LexicalCommand<{
|
||||
@@ -64,7 +63,7 @@ export const BlocksDrawerComponent: React.FC = () => {
|
||||
const [replaceNodeKey, setReplaceNodeKey] = useState<null | string>(null)
|
||||
const editDepth = useEditDepth()
|
||||
const { t } = useTranslation('fields')
|
||||
const { closeModal, openModal } = useModal()
|
||||
const { openModal } = useModal()
|
||||
|
||||
const labels = {
|
||||
plural: t('blocks') || 'Blocks',
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import 'payload/scss';
|
||||
@@ -6,10 +6,8 @@ import { formatLabels, getTranslation } from 'payload/utilities'
|
||||
import type { FeatureProvider } from '../types'
|
||||
|
||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||
import { BlockIcon } from '../../lexical/ui/icons/Block'
|
||||
import './index.scss'
|
||||
import { BlockNode } from './nodes/BlocksNode'
|
||||
import { BlocksPlugin, INSERT_BLOCK_COMMAND } from './plugin'
|
||||
import { INSERT_BLOCK_COMMAND } from './plugin/commands'
|
||||
import { blockPopulationPromiseHOC } from './populationPromise'
|
||||
import { blockValidationHOC } from './validate'
|
||||
|
||||
@@ -43,7 +41,9 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
Component: BlocksPlugin,
|
||||
Component: () =>
|
||||
// @ts-expect-error
|
||||
import('./plugin').then((module) => module.BlocksPlugin),
|
||||
position: 'normal',
|
||||
},
|
||||
],
|
||||
@@ -56,7 +56,9 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
|
||||
options: [
|
||||
...props.blocks.map((block) => {
|
||||
return new SlashMenuOption('block-' + block.slug, {
|
||||
Icon: BlockIcon,
|
||||
Icon: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/Block').then((module) => module.BlockIcon),
|
||||
displayName: ({ i18n }) => {
|
||||
return getTranslation(block.labels.singular, i18n)
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@ import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||
import ObjectID from 'bson-objectid'
|
||||
import React from 'react'
|
||||
|
||||
import { BlockComponent } from '../component'
|
||||
import { transformInputFormData } from '../utils/transformInputFormData'
|
||||
|
||||
export type BlockFields = {
|
||||
@@ -25,6 +24,13 @@ export type BlockFields = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const BlockComponent = React.lazy(() =>
|
||||
// @ts-expect-error TypeScript being dumb
|
||||
import('../component').then((module) => ({
|
||||
default: module.BlockComponent,
|
||||
})),
|
||||
)
|
||||
|
||||
export type SerializedBlockNode = Spread<
|
||||
{
|
||||
fields: BlockFields
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { LexicalCommand } from 'lexical'
|
||||
|
||||
import { createCommand } from 'lexical'
|
||||
|
||||
import type { InsertBlockPayload } from './index'
|
||||
|
||||
export const INSERT_BLOCK_COMMAND: LexicalCommand<InsertBlockPayload> =
|
||||
createCommand('INSERT_BLOCK_COMMAND')
|
||||
@@ -1,19 +1,17 @@
|
||||
'use client'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { $insertNodeToNearestRoot, mergeRegister } from '@lexical/utils'
|
||||
import { COMMAND_PRIORITY_EDITOR, type LexicalCommand, createCommand } from 'lexical'
|
||||
import { COMMAND_PRIORITY_EDITOR } from 'lexical'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
import type { BlockFields } from '../nodes/BlocksNode'
|
||||
|
||||
import { BlocksDrawerComponent } from '../drawer'
|
||||
import { $createBlockNode, BlockNode } from '../nodes/BlocksNode'
|
||||
import { INSERT_BLOCK_COMMAND } from './commands'
|
||||
|
||||
export type InsertBlockPayload = Exclude<BlockFields, 'id'>
|
||||
|
||||
export const INSERT_BLOCK_COMMAND: LexicalCommand<InsertBlockPayload> =
|
||||
createCommand('INSERT_BLOCK_COMMAND')
|
||||
|
||||
export function BlocksPlugin(): JSX.Element | null {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { HeadingTagType, SerializedHeadingNode } from '@lexical/rich-text'
|
||||
import type React from 'react'
|
||||
|
||||
import { $createHeadingNode, HeadingNode } from '@lexical/rich-text'
|
||||
import { $setBlocksType } from '@lexical/selection'
|
||||
@@ -9,12 +8,6 @@ import type { HTMLConverter } from '../converters/html/converter/types'
|
||||
import type { FeatureProvider } from '../types'
|
||||
|
||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||
import { H1Icon } from '../../lexical/ui/icons/H1'
|
||||
import { H2Icon } from '../../lexical/ui/icons/H2'
|
||||
import { H3Icon } from '../../lexical/ui/icons/H3'
|
||||
import { H4Icon } from '../../lexical/ui/icons/H4'
|
||||
import { H5Icon } from '../../lexical/ui/icons/H5'
|
||||
import { H6Icon } from '../../lexical/ui/icons/H6'
|
||||
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
|
||||
import { convertLexicalNodesToHTML } from '../converters/html/converter'
|
||||
import { MarkdownTransformer } from './markdownTransformer'
|
||||
@@ -30,13 +23,19 @@ type Props = {
|
||||
enabledHeadingSizes?: HeadingTagType[]
|
||||
}
|
||||
|
||||
const HeadingToIconMap: Record<HeadingTagType, React.FC> = {
|
||||
h1: H1Icon,
|
||||
h2: H2Icon,
|
||||
h3: H3Icon,
|
||||
h4: H4Icon,
|
||||
h5: H5Icon,
|
||||
h6: H6Icon,
|
||||
const iconImports = {
|
||||
// @ts-expect-error
|
||||
h1: () => import('../../lexical/ui/icons/H1').then((module) => module.H1Icon),
|
||||
// @ts-expect-error
|
||||
h2: () => import('../../lexical/ui/icons/H2').then((module) => module.H2Icon),
|
||||
// @ts-expect-error
|
||||
h3: () => import('../../lexical/ui/icons/H3').then((module) => module.H3Icon),
|
||||
// @ts-expect-error
|
||||
h4: () => import('../../lexical/ui/icons/H4').then((module) => module.H4Icon),
|
||||
// @ts-expect-error
|
||||
h5: () => import('../../lexical/ui/icons/H5').then((module) => module.H5Icon),
|
||||
// @ts-expect-error
|
||||
h6: () => import('../../lexical/ui/icons/H6').then((module) => module.H6Icon),
|
||||
}
|
||||
|
||||
export const HeadingFeature = (props: Props): FeatureProvider => {
|
||||
@@ -50,7 +49,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
|
||||
...enabledHeadingSizes.map((headingSize, i) =>
|
||||
TextDropdownSectionWithEntries([
|
||||
{
|
||||
ChildComponent: HeadingToIconMap[headingSize],
|
||||
ChildComponent: iconImports[headingSize],
|
||||
isActive: () => false,
|
||||
key: headingSize,
|
||||
label: `Heading ${headingSize.charAt(1)}`,
|
||||
@@ -98,7 +97,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
|
||||
key: 'basic',
|
||||
options: [
|
||||
new SlashMenuOption(`heading-${headingSize.charAt(1)}`, {
|
||||
Icon: HeadingToIconMap[headingSize],
|
||||
Icon: iconImports[headingSize],
|
||||
displayName: `Heading ${headingSize.charAt(1)}`,
|
||||
keywords: ['heading', headingSize],
|
||||
onSelect: () => {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import 'payload/scss';
|
||||
@@ -4,25 +4,18 @@ import type { Field } from 'payload/types'
|
||||
|
||||
import { $findMatchingParent } from '@lexical/utils'
|
||||
import { $getSelection, $isRangeSelection } from 'lexical'
|
||||
import { withMergedProps } from 'payload/utilities'
|
||||
|
||||
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 { LinkIcon } from '../../lexical/ui/icons/Link'
|
||||
import { getSelectedNode } from '../../lexical/utils/getSelectedNode'
|
||||
import { FeaturesSectionWithEntries } from '../common/floatingSelectToolbarFeaturesButtonsSection'
|
||||
import { convertLexicalNodesToHTML } from '../converters/html/converter'
|
||||
import './index.scss'
|
||||
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'
|
||||
import { LinkPlugin } from './plugins/link'
|
||||
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './plugins/floatingLinkEditor/LinkEditor/commands'
|
||||
import { linkPopulationPromiseHOC } from './populationPromise'
|
||||
|
||||
export type LinkFeatureProps = {
|
||||
@@ -38,7 +31,9 @@ export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
|
||||
sections: [
|
||||
FeaturesSectionWithEntries([
|
||||
{
|
||||
ChildComponent: LinkIcon,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/Link').then((module) => module.LinkIcon),
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
const selectedNode = getSelectedNode(selection)
|
||||
@@ -134,22 +129,35 @@ export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
Component: LinkPlugin,
|
||||
Component: () =>
|
||||
// @ts-expect-error
|
||||
import('./plugins/link').then((module) => module.LinkPlugin),
|
||||
position: 'normal',
|
||||
},
|
||||
{
|
||||
Component: AutoLinkPlugin,
|
||||
Component: () =>
|
||||
// @ts-expect-error
|
||||
import('./plugins/autoLink').then((module) => module.AutoLinkPlugin),
|
||||
position: 'normal',
|
||||
},
|
||||
{
|
||||
Component: ClickableLinkPlugin,
|
||||
Component: () =>
|
||||
// @ts-expect-error
|
||||
import('./plugins/clickableLink').then((module) => module.ClickableLinkPlugin),
|
||||
position: 'normal',
|
||||
},
|
||||
{
|
||||
Component: withMergedProps({
|
||||
Component: FloatingLinkEditorPlugin,
|
||||
Component: () =>
|
||||
// @ts-expect-error
|
||||
import('./plugins/floatingLinkEditor').then((module) => {
|
||||
const floatingLinkEditorPlugin = module.FloatingLinkEditorPlugin
|
||||
return import('payload/utilities').then((module) =>
|
||||
module.withMergedProps({
|
||||
Component: floatingLinkEditorPlugin,
|
||||
toMergeIntoProps: props,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
position: 'floatingAnchorElem',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { LexicalCommand } from 'lexical'
|
||||
|
||||
import { createCommand } from 'lexical'
|
||||
|
||||
import type { LinkPayload } from '../types'
|
||||
|
||||
export const TOGGLE_LINK_WITH_MODAL_COMMAND: LexicalCommand<LinkPayload | null> = createCommand(
|
||||
'TOGGLE_LINK_WITH_MODAL_COMMAND',
|
||||
)
|
||||
@@ -1,5 +1,4 @@
|
||||
'use client'
|
||||
import type { LexicalCommand } from 'lexical'
|
||||
import type { Data, Fields } from 'payload/types'
|
||||
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
COMMAND_PRIORITY_LOW,
|
||||
KEY_ESCAPE_COMMAND,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
import { formatDrawerSlug } from 'payload/components/elements'
|
||||
import {
|
||||
@@ -38,10 +36,7 @@ import { setFloatingElemPositionForLinkEditor } from '../../../../../lexical/uti
|
||||
import { LinkDrawer } from '../../../drawer'
|
||||
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '../../../nodes/LinkNode'
|
||||
import { transformExtraFields } from '../utilities'
|
||||
|
||||
export const TOGGLE_LINK_WITH_MODAL_COMMAND: LexicalCommand<LinkPayload | null> = createCommand(
|
||||
'TOGGLE_LINK_WITH_MODAL_COMMAND',
|
||||
)
|
||||
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './commands'
|
||||
|
||||
export function LinkEditor({
|
||||
anchorElem,
|
||||
|
||||
@@ -4,19 +4,20 @@ import { $createParagraphNode, $getSelection, $isRangeSelection } from 'lexical'
|
||||
import type { FeatureProvider } from '../types'
|
||||
|
||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||
import { TextIcon } from '../../lexical/ui/icons/Text'
|
||||
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
|
||||
|
||||
export const ParagraphFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
TextDropdownSectionWithEntries([
|
||||
{
|
||||
ChildComponent: TextIcon,
|
||||
isActive: ({ editor, selection }) => false,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/Text').then((module) => module.TextIcon),
|
||||
isActive: () => false,
|
||||
key: 'normal-text',
|
||||
label: 'Normal Text',
|
||||
onClick: ({ editor }) => {
|
||||
@@ -40,7 +41,9 @@ export const ParagraphFeature = (): FeatureProvider => {
|
||||
key: 'basic',
|
||||
options: [
|
||||
new SlashMenuOption('paragraph', {
|
||||
Icon: TextIcon,
|
||||
Icon: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/Text').then((module) => module.TextIcon),
|
||||
displayName: 'Paragraph',
|
||||
keywords: ['normal', 'paragraph', 'p', 'text'],
|
||||
onSelect: ({ editor }) => {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { LexicalCommand } from 'lexical'
|
||||
|
||||
import { createCommand } from 'lexical'
|
||||
|
||||
export const INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND: LexicalCommand<{
|
||||
replace: { nodeKey: string } | false
|
||||
}> = createCommand('INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND')
|
||||
@@ -1 +0,0 @@
|
||||
@import 'payload/scss';
|
||||
@@ -1,26 +1,16 @@
|
||||
'use client'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import {
|
||||
$getNodeByKey,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
type LexicalCommand,
|
||||
type LexicalEditor,
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
import { $getNodeByKey, COMMAND_PRIORITY_EDITOR, type LexicalEditor } from 'lexical'
|
||||
import { useListDrawer } from 'payload/components/elements'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { $createRelationshipNode } from '../nodes/RelationshipNode'
|
||||
import { INSERT_RELATIONSHIP_COMMAND } from '../plugins'
|
||||
import { EnabledRelationshipsCondition } from '../utils/EnabledRelationshipsCondition'
|
||||
import './index.scss'
|
||||
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './commands'
|
||||
|
||||
const baseClass = 'lexical-relationship-drawer'
|
||||
|
||||
export const INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND: LexicalCommand<{
|
||||
replace: { nodeKey: string } | false
|
||||
}> = createCommand('INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND')
|
||||
|
||||
const insertRelationship = ({
|
||||
id,
|
||||
editor,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import 'payload/scss';
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { FeatureProvider } from '../types'
|
||||
|
||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||
import { RelationshipIcon } from '../../lexical/ui/icons/Relationship'
|
||||
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './drawer'
|
||||
import './index.scss'
|
||||
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './drawer/commands'
|
||||
import { RelationshipNode } from './nodes/RelationshipNode'
|
||||
import RelationshipPlugin from './plugins'
|
||||
import { relationshipPopulationPromise } from './populationPromise'
|
||||
|
||||
export const RelationshipFeature = (): FeatureProvider => {
|
||||
@@ -22,7 +19,9 @@ export const RelationshipFeature = (): FeatureProvider => {
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
Component: RelationshipPlugin,
|
||||
Component: () =>
|
||||
// @ts-expect-error
|
||||
import('./plugins').then((module) => module.RelationshipPlugin),
|
||||
position: 'normal',
|
||||
},
|
||||
],
|
||||
@@ -34,7 +33,11 @@ export const RelationshipFeature = (): FeatureProvider => {
|
||||
key: 'basic',
|
||||
options: [
|
||||
new SlashMenuOption('relationship', {
|
||||
Icon: RelationshipIcon,
|
||||
Icon: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/Relationship').then(
|
||||
(module) => module.RelationshipIcon,
|
||||
),
|
||||
displayName: 'Relationship',
|
||||
keywords: ['relationship', 'relation', 'rel'],
|
||||
onSelect: ({ editor }) => {
|
||||
|
||||
@@ -16,7 +16,12 @@ import {
|
||||
} from '@lexical/react/LexicalDecoratorBlockNode'
|
||||
import * as React from 'react'
|
||||
|
||||
import { RelationshipComponent } from './components/RelationshipComponent'
|
||||
const RelationshipComponent = React.lazy(() =>
|
||||
// @ts-expect-error TypeScript being dumb
|
||||
import('./components/RelationshipComponent').then((module) => ({
|
||||
default: module.RelationshipComponent,
|
||||
})),
|
||||
)
|
||||
|
||||
export type RelationshipData = {
|
||||
relationTo: string
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import type { RelationshipData } from '../RelationshipNode'
|
||||
|
||||
import { useEditorConfigContext } from '../../../../lexical/config/EditorConfigProvider'
|
||||
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from '../../drawer'
|
||||
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from '../../drawer/commands'
|
||||
import { EnabledRelationshipsCondition } from '../../utils/EnabledRelationshipsCondition'
|
||||
import './index.scss'
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const INSERT_RELATIONSHIP_COMMAND: LexicalCommand<RelationshipData> = cre
|
||||
'INSERT_RELATIONSHIP_COMMAND',
|
||||
)
|
||||
|
||||
export default function RelationshipPlugin(): JSX.Element | null {
|
||||
export function RelationshipPlugin(): JSX.Element | null {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const { collections } = useConfig()
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { UploadData } from '../nodes/UploadNode'
|
||||
|
||||
import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider'
|
||||
import { EnabledRelationshipsCondition } from '../../Relationship/utils/EnabledRelationshipsCondition'
|
||||
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from '../drawer'
|
||||
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from '../drawer/commands'
|
||||
import { ExtraFieldsUploadDrawer } from './ExtraFieldsDrawer'
|
||||
import './index.scss'
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { LexicalCommand } from 'lexical'
|
||||
|
||||
import { createCommand } from 'lexical'
|
||||
|
||||
export const INSERT_UPLOAD_WITH_DRAWER_COMMAND: LexicalCommand<{
|
||||
replace: { nodeKey: string } | false
|
||||
}> = createCommand('INSERT_UPLOAD_WITH_DRAWER_COMMAND')
|
||||
@@ -1 +0,0 @@
|
||||
@import 'payload/scss';
|
||||
@@ -1,26 +1,16 @@
|
||||
'use client'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import {
|
||||
$getNodeByKey,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
type LexicalCommand,
|
||||
type LexicalEditor,
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
import { $getNodeByKey, COMMAND_PRIORITY_EDITOR, type LexicalEditor } from 'lexical'
|
||||
import { useListDrawer } from 'payload/components/elements'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { EnabledRelationshipsCondition } from '../../Relationship/utils/EnabledRelationshipsCondition'
|
||||
import { $createUploadNode } from '../nodes/UploadNode'
|
||||
import { INSERT_UPLOAD_COMMAND } from '../plugin'
|
||||
import './index.scss'
|
||||
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './commands'
|
||||
|
||||
const baseClass = 'lexical-upload-drawer'
|
||||
|
||||
export const INSERT_UPLOAD_WITH_DRAWER_COMMAND: LexicalCommand<{
|
||||
replace: { nodeKey: string } | false
|
||||
}> = createCommand('INSERT_UPLOAD_WITH_DRAWER_COMMAND')
|
||||
|
||||
const insertUpload = ({
|
||||
id,
|
||||
editor,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import 'payload/scss';
|
||||
@@ -7,11 +7,8 @@ import type { FeatureProvider } from '../types'
|
||||
import type { SerializedUploadNode } from './nodes/UploadNode'
|
||||
|
||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||
import { UploadIcon } from '../../lexical/ui/icons/Upload'
|
||||
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './drawer'
|
||||
import './index.scss'
|
||||
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './drawer/commands'
|
||||
import { UploadNode } from './nodes/UploadNode'
|
||||
import { UploadPlugin } from './plugin'
|
||||
import { uploadPopulationPromiseHOC } from './populationPromise'
|
||||
import { uploadValidation } from './validate'
|
||||
|
||||
@@ -55,7 +52,9 @@ export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => {
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
Component: UploadPlugin,
|
||||
Component: () =>
|
||||
// @ts-expect-error
|
||||
import('./plugin').then((module) => module.UploadPlugin),
|
||||
position: 'normal',
|
||||
},
|
||||
],
|
||||
@@ -67,7 +66,9 @@ export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => {
|
||||
key: 'basic',
|
||||
options: [
|
||||
new SlashMenuOption('upload', {
|
||||
Icon: UploadIcon,
|
||||
Icon: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/Upload').then((module) => module.UploadIcon),
|
||||
displayName: 'Upload',
|
||||
keywords: ['upload', 'image', 'file', 'img', 'picture', 'photo', 'media'],
|
||||
onSelect: ({ editor }) => {
|
||||
|
||||
@@ -3,14 +3,13 @@ import type {
|
||||
FloatingToolbarSectionEntry,
|
||||
} from '../../lexical/plugins/FloatingSelectToolbar/types'
|
||||
|
||||
import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft'
|
||||
import './index.scss'
|
||||
|
||||
export const AlignDropdownSectionWithEntries = (
|
||||
entries: FloatingToolbarSectionEntry[],
|
||||
): FloatingToolbarSection => {
|
||||
return {
|
||||
ChildComponent: AlignLeftIcon,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/AlignLeft').then((module) => module.AlignLeftIcon),
|
||||
entries,
|
||||
key: 'dropdown-align',
|
||||
order: 2,
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.floating-select-toolbar-popup__section-dropdown-align {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
@@ -2,21 +2,20 @@ import { FORMAT_ELEMENT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../types'
|
||||
|
||||
import { AlignCenterIcon } from '../../lexical/ui/icons/AlignCenter'
|
||||
import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft'
|
||||
import { AlignDropdownSectionWithEntries } from './floatingSelectToolbarAlignDropdownSection'
|
||||
import './index.scss'
|
||||
|
||||
export const AlignFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
AlignDropdownSectionWithEntries([
|
||||
{
|
||||
ChildComponent: AlignLeftIcon,
|
||||
isActive: ({ editor, selection }) => false,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/AlignLeft').then((module) => module.AlignLeftIcon),
|
||||
isActive: () => false,
|
||||
key: 'align-left',
|
||||
label: `Align Left`,
|
||||
onClick: ({ editor }) => {
|
||||
@@ -27,8 +26,12 @@ export const AlignFeature = (): FeatureProvider => {
|
||||
]),
|
||||
AlignDropdownSectionWithEntries([
|
||||
{
|
||||
ChildComponent: AlignCenterIcon,
|
||||
isActive: ({ editor, selection }) => false,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/AlignCenter').then(
|
||||
(module) => module.AlignCenterIcon,
|
||||
),
|
||||
isActive: () => false,
|
||||
key: 'align-center',
|
||||
label: `Align Center`,
|
||||
onClick: ({ editor }) => {
|
||||
@@ -39,8 +42,12 @@ export const AlignFeature = (): FeatureProvider => {
|
||||
]),
|
||||
AlignDropdownSectionWithEntries([
|
||||
{
|
||||
ChildComponent: AlignLeftIcon,
|
||||
isActive: ({ editor, selection }) => false,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/AlignRight').then(
|
||||
(module) => module.AlignRightIcon,
|
||||
),
|
||||
isActive: () => false,
|
||||
key: 'align-right',
|
||||
label: `Align Right`,
|
||||
onClick: ({ editor }) => {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.floating-select-toolbar-popup__section-features {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
@@ -3,8 +3,6 @@ import type {
|
||||
FloatingToolbarSectionEntry,
|
||||
} from '../../../lexical/plugins/FloatingSelectToolbar/types'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
export const FeaturesSectionWithEntries = (
|
||||
entries: FloatingToolbarSectionEntry[],
|
||||
): FloatingToolbarSection => {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.floating-select-toolbar-popup__section-dropdown-text {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
@@ -3,14 +3,13 @@ import type {
|
||||
FloatingToolbarSectionEntry,
|
||||
} from '../../../lexical/plugins/FloatingSelectToolbar/types'
|
||||
|
||||
import { TextIcon } from '../../../lexical/ui/icons/Text'
|
||||
import './index.scss'
|
||||
|
||||
export const TextDropdownSectionWithEntries = (
|
||||
entries: FloatingToolbarSectionEntry[],
|
||||
): FloatingToolbarSection => {
|
||||
return {
|
||||
ChildComponent: TextIcon,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../../lexical/ui/icons/Text').then((module) => module.TextIcon),
|
||||
entries,
|
||||
key: 'dropdown-text',
|
||||
order: 1,
|
||||
|
||||
@@ -15,10 +15,6 @@ export const HTMLConverterFeature = (props?: HTMLConverterFeatureProps): Feature
|
||||
if (!props) {
|
||||
props = {}
|
||||
}
|
||||
/*const defaultConvertersWithConvertersFromFeatures = defaultConverters
|
||||
defaultConvertersWithConver tersFromFeatures.set(props?
|
||||
|
||||
*/
|
||||
|
||||
return {
|
||||
feature: () => {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { FeatureProvider } from '../../types'
|
||||
|
||||
import { TestRecorderPlugin } from './plugin'
|
||||
|
||||
export const TestRecorderFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
plugins: [
|
||||
{
|
||||
Component: TestRecorderPlugin,
|
||||
Component: () =>
|
||||
// @ts-expect-error
|
||||
import('./plugin').then((module) => module.TestRecorderPlugin),
|
||||
position: 'bottom',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import type { FeatureProvider } from '../../types'
|
||||
|
||||
import './index.scss'
|
||||
import { TreeViewPlugin } from './plugin'
|
||||
|
||||
export const TreeviewFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
plugins: [
|
||||
{
|
||||
Component: TreeViewPlugin,
|
||||
Component: () =>
|
||||
// @ts-expect-error
|
||||
import('./plugin').then((module) => module.TreeViewPlugin),
|
||||
position: 'bottom',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
||||
import { TreeView } from '@lexical/react/LexicalTreeView'
|
||||
import * as React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
export function TreeViewPlugin(): JSX.Element {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,6 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../../types'
|
||||
|
||||
import { BoldIcon } from '../../../lexical/ui/icons/Bold'
|
||||
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
|
||||
import {
|
||||
BOLD_ITALIC_STAR,
|
||||
@@ -25,8 +24,10 @@ export const BoldTextFeature = (): FeatureProvider => {
|
||||
sections: [
|
||||
SectionWithEntries([
|
||||
{
|
||||
ChildComponent: BoldIcon,
|
||||
isActive: ({ editor, selection }) => {
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../../lexical/ui/icons/Bold').then((module) => module.BoldIcon),
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
return selection.hasFormat('bold')
|
||||
}
|
||||
|
||||
@@ -2,20 +2,21 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../../types'
|
||||
|
||||
import { CodeIcon } from '../../../lexical/ui/icons/Code'
|
||||
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
|
||||
import { INLINE_CODE } from './markdownTransformers'
|
||||
|
||||
export const InlineCodeTextFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: ({ featureProviderMap }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
SectionWithEntries([
|
||||
{
|
||||
ChildComponent: CodeIcon,
|
||||
isActive: ({ editor, selection }) => {
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../../lexical/ui/icons/Code').then((module) => module.CodeIcon),
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
return selection.hasFormat('code')
|
||||
}
|
||||
|
||||
@@ -2,20 +2,21 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../../types'
|
||||
|
||||
import { ItalicIcon } from '../../../lexical/ui/icons/Italic'
|
||||
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
|
||||
import { ITALIC_STAR, ITALIC_UNDERSCORE } from './markdownTransformers'
|
||||
|
||||
export const ItalicTextFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
SectionWithEntries([
|
||||
{
|
||||
ChildComponent: ItalicIcon,
|
||||
isActive: ({ editor, selection }) => {
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../../lexical/ui/icons/Italic').then((module) => module.ItalicIcon),
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
return selection.hasFormat('italic')
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ import type {
|
||||
FloatingToolbarSectionEntry,
|
||||
} from '../../../lexical/plugins/FloatingSelectToolbar/types'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
export const SectionWithEntries = (
|
||||
entries: FloatingToolbarSectionEntry[],
|
||||
): FloatingToolbarSection => {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.floating-select-toolbar-popup__section-format {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
@@ -2,20 +2,23 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../../types'
|
||||
|
||||
import { StrikethroughIcon } from '../../../lexical/ui/icons/Strikethrough'
|
||||
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
|
||||
import { STRIKETHROUGH } from './markdownTransformers'
|
||||
|
||||
export const StrikethroughTextFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
SectionWithEntries([
|
||||
{
|
||||
ChildComponent: StrikethroughIcon,
|
||||
isActive: ({ editor, selection }) => {
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../../lexical/ui/icons/Strikethrough').then(
|
||||
(module) => module.StrikethroughIcon,
|
||||
),
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
return selection.hasFormat('strikethrough')
|
||||
}
|
||||
|
||||
@@ -2,19 +2,22 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../../types'
|
||||
|
||||
import { SubscriptIcon } from '../../../lexical/ui/icons/Subscript'
|
||||
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
|
||||
|
||||
export const SubscriptTextFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
SectionWithEntries([
|
||||
{
|
||||
ChildComponent: SubscriptIcon,
|
||||
isActive: ({ editor, selection }) => {
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../../lexical/ui/icons/Subscript').then(
|
||||
(module) => module.SubscriptIcon,
|
||||
),
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
return selection.hasFormat('subscript')
|
||||
}
|
||||
|
||||
@@ -2,19 +2,22 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../../types'
|
||||
|
||||
import { SuperscriptIcon } from '../../../lexical/ui/icons/Superscript'
|
||||
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
|
||||
|
||||
export const SuperscriptTextFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
SectionWithEntries([
|
||||
{
|
||||
ChildComponent: SuperscriptIcon,
|
||||
isActive: ({ editor, selection }) => {
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../../lexical/ui/icons/Superscript').then(
|
||||
(module) => module.SuperscriptIcon,
|
||||
),
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
return selection.hasFormat('superscript')
|
||||
}
|
||||
|
||||
@@ -2,19 +2,22 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../../types'
|
||||
|
||||
import { UnderlineIcon } from '../../../lexical/ui/icons/Underline'
|
||||
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
|
||||
|
||||
export const UnderlineTextFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
SectionWithEntries([
|
||||
{
|
||||
ChildComponent: UnderlineIcon,
|
||||
isActive: ({ editor, selection }) => {
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../../lexical/ui/icons/Underline').then(
|
||||
(module) => module.UnderlineIcon,
|
||||
),
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
return selection.hasFormat('underline')
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ import type {
|
||||
FloatingToolbarSectionEntry,
|
||||
} from '../../lexical/plugins/FloatingSelectToolbar/types'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
export const IndentSectionWithEntries = (
|
||||
entries: FloatingToolbarSectionEntry[],
|
||||
): FloatingToolbarSection => {
|
||||
|
||||
@@ -2,10 +2,7 @@ import { INDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../types'
|
||||
|
||||
import { IndentDecreaseIcon } from '../../lexical/ui/icons/IndentDecrease'
|
||||
import { IndentIncreaseIcon } from '../../lexical/ui/icons/IndentIncrease'
|
||||
import { IndentSectionWithEntries } from './floatingSelectToolbarIndentSection'
|
||||
import './index.scss'
|
||||
|
||||
export const IndentFeature = (): FeatureProvider => {
|
||||
return {
|
||||
@@ -15,9 +12,13 @@ export const IndentFeature = (): FeatureProvider => {
|
||||
sections: [
|
||||
IndentSectionWithEntries([
|
||||
{
|
||||
ChildComponent: IndentDecreaseIcon,
|
||||
isActive: ({ editor, selection }) => false,
|
||||
isEnabled: ({ editor, selection }) => {
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/IndentDecrease').then(
|
||||
(module) => module.IndentDecreaseIcon,
|
||||
),
|
||||
isActive: () => false,
|
||||
isEnabled: ({ selection }) => {
|
||||
if (!selection || !selection?.getNodes()?.length) {
|
||||
return false
|
||||
}
|
||||
@@ -39,8 +40,12 @@ export const IndentFeature = (): FeatureProvider => {
|
||||
]),
|
||||
IndentSectionWithEntries([
|
||||
{
|
||||
ChildComponent: IndentIncreaseIcon,
|
||||
isActive: ({ editor, selection }) => false,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/IndentIncrease').then(
|
||||
(module) => module.IndentIncreaseIcon,
|
||||
),
|
||||
isActive: () => false,
|
||||
key: 'indent-increase',
|
||||
label: `Increase Indent`,
|
||||
onClick: ({ editor }) => {
|
||||
@@ -51,6 +56,14 @@ export const IndentFeature = (): FeatureProvider => {
|
||||
]),
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
Component: () =>
|
||||
// @ts-expect-error
|
||||
import('./plugin').then((module) => module.IndentPlugin),
|
||||
position: 'normal',
|
||||
},
|
||||
],
|
||||
props: null,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
export function IndentPlugin(): null {
|
||||
return null
|
||||
}
|
||||
@@ -3,11 +3,9 @@ import { INSERT_CHECK_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list
|
||||
import type { FeatureProvider } from '../../types'
|
||||
|
||||
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||
import { ChecklistIcon } from '../../../lexical/ui/icons/Checklist'
|
||||
import { TextDropdownSectionWithEntries } from '../../common/floatingSelectToolbarTextDropdownSection'
|
||||
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
|
||||
import { CHECK_LIST } from './markdownTransformers'
|
||||
import { LexicalCheckListPlugin } from './plugin'
|
||||
|
||||
// 345
|
||||
// carbs 7
|
||||
@@ -19,7 +17,11 @@ export const CheckListFeature = (): FeatureProvider => {
|
||||
sections: [
|
||||
TextDropdownSectionWithEntries([
|
||||
{
|
||||
ChildComponent: ChecklistIcon,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../../lexical/ui/icons/Checklist').then(
|
||||
(module) => module.ChecklistIcon,
|
||||
),
|
||||
isActive: () => false,
|
||||
key: 'checkList',
|
||||
label: `Check List`,
|
||||
@@ -53,7 +55,9 @@ export const CheckListFeature = (): FeatureProvider => {
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
Component: LexicalCheckListPlugin,
|
||||
Component: () =>
|
||||
// @ts-expect-error
|
||||
import('./plugin').then((module) => module.LexicalCheckListPlugin),
|
||||
position: 'normal',
|
||||
},
|
||||
],
|
||||
@@ -65,7 +69,11 @@ export const CheckListFeature = (): FeatureProvider => {
|
||||
key: 'lists',
|
||||
options: [
|
||||
new SlashMenuOption('checklist', {
|
||||
Icon: ChecklistIcon,
|
||||
Icon: () =>
|
||||
// @ts-expect-error
|
||||
import('../../../lexical/ui/icons/Checklist').then(
|
||||
(module) => module.ChecklistIcon,
|
||||
),
|
||||
displayName: 'Check List',
|
||||
keywords: ['check list', 'check', 'checklist', 'cl'],
|
||||
onSelect: ({ editor }) => {
|
||||
|
||||
@@ -3,10 +3,8 @@ import { INSERT_ORDERED_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/li
|
||||
import type { FeatureProvider } from '../../types'
|
||||
|
||||
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||
import { OrderedListIcon } from '../../../lexical/ui/icons/OrderedList'
|
||||
import { TextDropdownSectionWithEntries } from '../../common/floatingSelectToolbarTextDropdownSection'
|
||||
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
|
||||
import { LexicalListPlugin } from '../plugin'
|
||||
import { ORDERED_LIST } from './markdownTransformer'
|
||||
|
||||
export const OrderedListFeature = (): FeatureProvider => {
|
||||
@@ -17,7 +15,11 @@ export const OrderedListFeature = (): FeatureProvider => {
|
||||
sections: [
|
||||
TextDropdownSectionWithEntries([
|
||||
{
|
||||
ChildComponent: OrderedListIcon,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../../lexical/ui/icons/OrderedList').then(
|
||||
(module) => module.OrderedListIcon,
|
||||
),
|
||||
isActive: () => false,
|
||||
key: 'orderedList',
|
||||
label: `Ordered List`,
|
||||
@@ -52,7 +54,9 @@ export const OrderedListFeature = (): FeatureProvider => {
|
||||
? []
|
||||
: [
|
||||
{
|
||||
Component: LexicalListPlugin,
|
||||
Component: () =>
|
||||
// @ts-expect-error
|
||||
import('../plugin').then((module) => module.LexicalListPlugin),
|
||||
position: 'normal',
|
||||
},
|
||||
],
|
||||
@@ -64,7 +68,11 @@ export const OrderedListFeature = (): FeatureProvider => {
|
||||
key: 'lists',
|
||||
options: [
|
||||
new SlashMenuOption('orderedlist', {
|
||||
Icon: OrderedListIcon,
|
||||
Icon: () =>
|
||||
// @ts-expect-error
|
||||
import('../../../lexical/ui/icons/OrderedList').then(
|
||||
(module) => module.OrderedListIcon,
|
||||
),
|
||||
displayName: 'Ordered List',
|
||||
keywords: ['ordered list', 'ol'],
|
||||
onSelect: ({ editor }) => {
|
||||
|
||||
@@ -3,10 +3,8 @@ import { INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/
|
||||
import type { FeatureProvider } from '../../types'
|
||||
|
||||
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||
import { UnorderedListIcon } from '../../../lexical/ui/icons/UnorderedList'
|
||||
import { TextDropdownSectionWithEntries } from '../../common/floatingSelectToolbarTextDropdownSection'
|
||||
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
|
||||
import { LexicalListPlugin } from '../plugin'
|
||||
import { UNORDERED_LIST } from './markdownTransformer'
|
||||
|
||||
export const UnorderedListFeature = (): FeatureProvider => {
|
||||
@@ -17,7 +15,11 @@ export const UnorderedListFeature = (): FeatureProvider => {
|
||||
sections: [
|
||||
TextDropdownSectionWithEntries([
|
||||
{
|
||||
ChildComponent: UnorderedListIcon,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../../lexical/ui/icons/UnorderedList').then(
|
||||
(module) => module.UnorderedListIcon,
|
||||
),
|
||||
isActive: () => false,
|
||||
key: 'unorderedList',
|
||||
label: `Unordered List`,
|
||||
@@ -48,7 +50,9 @@ export const UnorderedListFeature = (): FeatureProvider => {
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
Component: LexicalListPlugin,
|
||||
Component: () =>
|
||||
// @ts-expect-error
|
||||
import('../plugin').then((module) => module.LexicalListPlugin),
|
||||
position: 'normal',
|
||||
},
|
||||
],
|
||||
@@ -60,7 +64,11 @@ export const UnorderedListFeature = (): FeatureProvider => {
|
||||
key: 'lists',
|
||||
options: [
|
||||
new SlashMenuOption('unorderedlist', {
|
||||
Icon: UnorderedListIcon,
|
||||
Icon: () =>
|
||||
// @ts-expect-error
|
||||
import('../../../lexical/ui/icons/UnorderedList').then(
|
||||
(module) => module.UnorderedListIcon,
|
||||
),
|
||||
displayName: 'Unordered List',
|
||||
keywords: ['unordered list', 'ul'],
|
||||
onSelect: ({ editor }) => {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { UnknownConvertedNodeData } from './index'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
type Props = {
|
||||
data: UnknownConvertedNodeData
|
||||
}
|
||||
|
||||
export const UnknownConvertedNodeComponent: React.FC<Props> = (props) => {
|
||||
const { data } = props
|
||||
|
||||
return (
|
||||
<div>
|
||||
Unknown converted payload-plugin-lexical node: <strong>{data?.nodeType}</strong>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,9 +2,7 @@ import type { SerializedLexicalNode, Spread } from 'lexical'
|
||||
|
||||
import { addClassNamesToElement } from '@lexical/utils'
|
||||
import { DecoratorNode, type EditorConfig, type LexicalNode, type NodeKey } from 'lexical'
|
||||
import React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
import * as React from 'react'
|
||||
|
||||
export type UnknownConvertedNodeData = {
|
||||
nodeData: unknown
|
||||
@@ -18,6 +16,13 @@ export type SerializedUnknownConvertedNode = Spread<
|
||||
SerializedLexicalNode
|
||||
>
|
||||
|
||||
const Component = React.lazy(() =>
|
||||
// @ts-expect-error TypeScript being dumb
|
||||
import('./Component').then((module) => ({
|
||||
default: module.UnknownConvertedNodeComponent,
|
||||
})),
|
||||
)
|
||||
|
||||
/** @noInheritDoc */
|
||||
export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
|
||||
__data: UnknownConvertedNodeData
|
||||
@@ -58,11 +63,7 @@ export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
|
||||
}
|
||||
|
||||
decorate(): JSX.Element | null {
|
||||
return (
|
||||
<div>
|
||||
Unknown converted payload-plugin-lexical node: <strong>{this.__data?.nodeType}</strong>
|
||||
</div>
|
||||
)
|
||||
return <Component data={this.__data} />
|
||||
}
|
||||
|
||||
exportJSON(): SerializedUnknownConvertedNode {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { UnknownConvertedNodeData } from './index'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
type Props = {
|
||||
data: UnknownConvertedNodeData
|
||||
}
|
||||
|
||||
export const UnknownConvertedNodeComponent: React.FC<Props> = (props) => {
|
||||
const { data } = props
|
||||
|
||||
return (
|
||||
<div>
|
||||
Unknown converted Slate node: <strong>{data?.nodeType}</strong>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,9 +2,7 @@ import type { SerializedLexicalNode, Spread } from 'lexical'
|
||||
|
||||
import { addClassNamesToElement } from '@lexical/utils'
|
||||
import { DecoratorNode, type EditorConfig, type LexicalNode, type NodeKey } from 'lexical'
|
||||
import React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
import * as React from 'react'
|
||||
|
||||
export type UnknownConvertedNodeData = {
|
||||
nodeData: unknown
|
||||
@@ -18,6 +16,13 @@ export type SerializedUnknownConvertedNode = Spread<
|
||||
SerializedLexicalNode
|
||||
>
|
||||
|
||||
const Component = React.lazy(() =>
|
||||
// @ts-expect-error TypeScript being dumb
|
||||
import('./Component').then((module) => ({
|
||||
default: module.UnknownConvertedNodeComponent,
|
||||
})),
|
||||
)
|
||||
|
||||
/** @noInheritDoc */
|
||||
export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
|
||||
__data: UnknownConvertedNodeData
|
||||
@@ -58,11 +63,7 @@ export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
|
||||
}
|
||||
|
||||
decorate(): JSX.Element | null {
|
||||
return (
|
||||
<div>
|
||||
Unknown converted Slate node: <strong>{this.__data?.nodeType}</strong>
|
||||
</div>
|
||||
)
|
||||
return <Component data={this.__data} />
|
||||
}
|
||||
|
||||
exportJSON(): SerializedUnknownConvertedNode {
|
||||
|
||||
@@ -99,23 +99,23 @@ export type Feature = {
|
||||
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: 'normal' // Determines at which position the Component will be added.
|
||||
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: 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 }>
|
||||
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: React.FC<{ anchorElem: HTMLElement }>
|
||||
position: 'floatingAnchorElem' // Determines at which position the Component will be added.
|
||||
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.
|
||||
}
|
||||
>
|
||||
|
||||
@@ -161,6 +161,33 @@ export type ResolvedFeatureMap = Map<string, ResolvedFeature>
|
||||
|
||||
export type FeatureProviderMap = Map<string, FeatureProvider>
|
||||
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
key: string
|
||||
position: 'top' // Determines at which position the Component will be added.
|
||||
}
|
||||
|
||||
export type SanitizedFeatures = Required<
|
||||
Pick<ResolvedFeature, 'markdownTransformers' | 'nodes'>
|
||||
> & {
|
||||
@@ -200,33 +227,7 @@ export type SanitizedFeatures = Required<
|
||||
}) => SerializedEditorState
|
||||
>
|
||||
}
|
||||
plugins?: Array<
|
||||
| {
|
||||
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
||||
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: 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: 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.
|
||||
}
|
||||
>
|
||||
plugins?: Array<SanitizedPlugin>
|
||||
/** The node types mapped to their populationPromises */
|
||||
populationPromises: Map<string, Array<PopulationPromise>>
|
||||
slashMenu: {
|
||||
|
||||
@@ -7,8 +7,10 @@ import type { FieldProps } from '../types'
|
||||
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
|
||||
const RichTextEditor = lazy(() => import('./Field'))
|
||||
|
||||
export const RichTextField: React.FC<FieldProps> = (props) => (
|
||||
export const RichTextField: React.FC<FieldProps> = (props) => {
|
||||
return (
|
||||
<Suspense fallback={<ShimmerEffect height="35vh" />}>
|
||||
<RichTextEditor {...props} />
|
||||
</Suspense>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
37
packages/richtext-lexical/src/field/lexical/EditorPlugin.tsx
Normal file
37
packages/richtext-lexical/src/field/lexical/EditorPlugin.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import type { SanitizedPlugin } from '../features/types'
|
||||
|
||||
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 (
|
||||
Component && (
|
||||
<React.Suspense>
|
||||
<Component />
|
||||
</React.Suspense>
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -39,3 +39,21 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.floating-select-toolbar-popup__section-dropdown-align {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
.floating-select-toolbar-popup__section-features {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
.floating-select-toolbar-popup__section-dropdown-text {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.floating-select-toolbar-popup__section-format {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useEffect, useState } from 'react'
|
||||
|
||||
import type { LexicalProviderProps } from './LexicalProvider'
|
||||
|
||||
import { EditorPlugin } from './EditorPlugin'
|
||||
import './LexicalEditor.scss'
|
||||
import { FloatingSelectToolbarPlugin } from './plugins/FloatingSelectToolbar'
|
||||
import { MarkdownShortcutPlugin } from './plugins/MarkdownShortcut'
|
||||
@@ -53,7 +54,7 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
|
||||
<React.Fragment>
|
||||
{editorConfig.features.plugins.map((plugin) => {
|
||||
if (plugin.position === 'top') {
|
||||
return <plugin.Component key={plugin.key} />
|
||||
return <EditorPlugin key={plugin.key} plugin={plugin} />
|
||||
}
|
||||
})}
|
||||
<RichTextPlugin
|
||||
@@ -65,10 +66,12 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
placeholder={<p className="editor-placeholder">Start typing, or press '/' for commands...</p>}
|
||||
placeholder={
|
||||
<p className="editor-placeholder">Start typing, or press '/' for commands...</p>
|
||||
}
|
||||
/>
|
||||
<OnChangePlugin
|
||||
// Selection changes can be ignore here, reducing the
|
||||
// Selection changes can be ignored here, reducing the
|
||||
// frequency that the FieldComponent and Payload receive updates.
|
||||
// Selection changes are only needed if you are saving selection state
|
||||
ignoreSelectionChange
|
||||
@@ -92,7 +95,9 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
|
||||
plugin.position === 'floatingAnchorElem' &&
|
||||
!(plugin.desktopOnly === true && isSmallWidthViewport)
|
||||
) {
|
||||
return <plugin.Component anchorElem={floatingAnchorElem} key={plugin.key} />
|
||||
return (
|
||||
<EditorPlugin anchorElem={floatingAnchorElem} key={plugin.key} plugin={plugin} />
|
||||
)
|
||||
}
|
||||
})}
|
||||
{editor.isEditable() && (
|
||||
@@ -113,12 +118,12 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
|
||||
<TabIndentationPlugin />
|
||||
{editorConfig.features.plugins.map((plugin) => {
|
||||
if (plugin.position === 'normal') {
|
||||
return <plugin.Component key={plugin.key} />
|
||||
return <EditorPlugin key={plugin.key} plugin={plugin} />
|
||||
}
|
||||
})}
|
||||
{editorConfig.features.plugins.map((plugin) => {
|
||||
if (plugin.position === 'bottom') {
|
||||
return <plugin.Component key={plugin.key} />
|
||||
return <EditorPlugin key={plugin.key} plugin={plugin} />
|
||||
}
|
||||
})}
|
||||
</React.Fragment>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { InitialConfigType } from '@lexical/react/LexicalComposer'
|
||||
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'
|
||||
@@ -24,6 +25,25 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
|
||||
const { editorConfig, fieldProps, onChange, path, readOnly } = props
|
||||
let { value } = props
|
||||
|
||||
const [initialConfig, setInitialConfig] = React.useState<InitialConfigType | null>(null)
|
||||
|
||||
// set lexical config in useffect async:
|
||||
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)
|
||||
})
|
||||
}, [editorConfig, readOnly, value])
|
||||
|
||||
if (editorConfig?.features?.hooks?.load?.length) {
|
||||
editorConfig.features.hooks.load.forEach((hook) => {
|
||||
value = hook({ incomingEditorState: value })
|
||||
@@ -49,15 +69,8 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
const initialConfig: InitialConfigType = {
|
||||
editable: readOnly === true ? false : true,
|
||||
editorState: value != null ? JSON.stringify(value) : undefined,
|
||||
namespace: editorConfig.lexical.namespace,
|
||||
nodes: [...getEnabledNodes({ editorConfig })],
|
||||
onError: (error: Error) => {
|
||||
throw error
|
||||
},
|
||||
theme: editorConfig.lexical.theme,
|
||||
if (!initialConfig) {
|
||||
return <p>Loading...</p>
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||
|
||||
import type { FeatureProvider } from '../../features/types'
|
||||
import type { EditorConfig, SanitizedEditorConfig } from './types'
|
||||
|
||||
@@ -21,7 +19,6 @@ import { IndentFeature } from '../../features/indent'
|
||||
import { CheckListFeature } from '../../features/lists/CheckList'
|
||||
import { OrderedListFeature } from '../../features/lists/OrderedList'
|
||||
import { UnorderedListFeature } from '../../features/lists/UnorderedList'
|
||||
import { LexicalEditorTheme } from '../theme/EditorTheme'
|
||||
import { sanitizeEditorConfig } from './sanitize'
|
||||
|
||||
export const defaultEditorFeatures: FeatureProvider[] = [
|
||||
@@ -46,14 +43,14 @@ export const defaultEditorFeatures: FeatureProvider[] = [
|
||||
//BlocksFeature(), // Adding this by default makes no sense if no blocks are defined
|
||||
]
|
||||
|
||||
export const defaultEditorLexicalConfig: LexicalEditorConfig = {
|
||||
namespace: 'lexical',
|
||||
theme: LexicalEditorTheme,
|
||||
}
|
||||
|
||||
export const defaultEditorConfig: EditorConfig = {
|
||||
features: defaultEditorFeatures,
|
||||
lexical: defaultEditorLexicalConfig,
|
||||
lexical: () =>
|
||||
// @ts-expect-error
|
||||
import('./defaultClient').then((module) => {
|
||||
const defaultEditorLexicalConfig = module.defaultEditorLexicalConfig
|
||||
return defaultEditorLexicalConfig
|
||||
}),
|
||||
}
|
||||
|
||||
export const defaultSanitizedEditorConfig: SanitizedEditorConfig =
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||
|
||||
import { LexicalEditorTheme } from '../theme/EditorTheme'
|
||||
|
||||
export const defaultEditorLexicalConfig: LexicalEditorConfig = {
|
||||
namespace: 'lexical',
|
||||
theme: LexicalEditorTheme,
|
||||
}
|
||||
@@ -4,11 +4,11 @@ import type { FeatureProvider, ResolvedFeatureMap, SanitizedFeatures } from '../
|
||||
|
||||
export type EditorConfig = {
|
||||
features: FeatureProvider[]
|
||||
lexical: LexicalEditorConfig
|
||||
lexical?: () => Promise<LexicalEditorConfig>
|
||||
}
|
||||
|
||||
export type SanitizedEditorConfig = {
|
||||
features: SanitizedFeatures
|
||||
lexical: LexicalEditorConfig
|
||||
lexical: () => Promise<LexicalEditorConfig>
|
||||
resolvedFeatureMap: ResolvedFeatureMap
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
const baseClass = 'floating-select-toolbar-popup__dropdown'
|
||||
|
||||
@@ -10,6 +10,57 @@ import type { FloatingToolbarSectionEntry } from '../types'
|
||||
import { DropDown, DropDownItem } from './DropDown'
|
||||
import './index.scss'
|
||||
|
||||
export const ToolbarEntry = ({
|
||||
anchorElem,
|
||||
editor,
|
||||
entry,
|
||||
}: {
|
||||
anchorElem: HTMLElement
|
||||
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>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropDownItem entry={entry} key={entry.key}>
|
||||
{ChildComponent && (
|
||||
<React.Suspense>
|
||||
<ChildComponent />
|
||||
</React.Suspense>
|
||||
)}
|
||||
<span className="text">{entry.label}</span>
|
||||
</DropDownItem>
|
||||
)
|
||||
}
|
||||
|
||||
export const ToolbarDropdown = ({
|
||||
Icon,
|
||||
anchorElem,
|
||||
@@ -31,21 +82,8 @@ export const ToolbarDropdown = ({
|
||||
>
|
||||
{entries.length &&
|
||||
entries.map((entry) => {
|
||||
if (entry.Component) {
|
||||
return (
|
||||
<entry.Component
|
||||
anchorElem={anchorElem}
|
||||
editor={editor}
|
||||
entry={entry}
|
||||
key={entry.key}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<DropDownItem entry={entry} key={entry.key}>
|
||||
<entry.ChildComponent />
|
||||
<span className="text">{entry.label}</span>
|
||||
</DropDownItem>
|
||||
<ToolbarEntry anchorElem={anchorElem} editor={editor} entry={entry} key={entry.key} />
|
||||
)
|
||||
})}
|
||||
</DropDown>
|
||||
|
||||
@@ -10,10 +10,12 @@ import {
|
||||
COMMAND_PRIORITY_LOW,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
} from 'lexical'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, 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 { getDOMRangeRect } from '../../utils/getDOMRangeRect'
|
||||
import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition'
|
||||
@@ -21,6 +23,117 @@ import { ToolbarButton } from './ToolbarButton'
|
||||
import { ToolbarDropdown } from './ToolbarDropdown'
|
||||
import './index.scss'
|
||||
|
||||
function ButtonSectionEntry({
|
||||
anchorElem,
|
||||
editor,
|
||||
entry,
|
||||
}: {
|
||||
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])
|
||||
|
||||
if (entry.Component) {
|
||||
return (
|
||||
Component && (
|
||||
<React.Suspense>
|
||||
<Component anchorElem={anchorElem} editor={editor} entry={entry} key={entry.key} />{' '}
|
||||
</React.Suspense>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarButton entry={entry} key={entry.key}>
|
||||
{ChildComponent && (
|
||||
<React.Suspense>
|
||||
<ChildComponent />
|
||||
</React.Suspense>
|
||||
)}
|
||||
</ToolbarButton>
|
||||
)
|
||||
}
|
||||
|
||||
function ToolbarSection({
|
||||
anchorElem,
|
||||
editor,
|
||||
index,
|
||||
section,
|
||||
}: {
|
||||
anchorElem: HTMLElement
|
||||
editor: LexicalEditor
|
||||
index: number
|
||||
section: FloatingToolbarSection
|
||||
}): JSX.Element {
|
||||
const { editorConfig } = useEditorConfigContext()
|
||||
|
||||
const Icon = useMemo(() => {
|
||||
return section?.type === 'dropdown' && section.entries.length && section.ChildComponent
|
||||
? React.lazy(() =>
|
||||
section.ChildComponent().then((resolvedComponent) => ({
|
||||
default: resolvedComponent,
|
||||
})),
|
||||
)
|
||||
: null
|
||||
}, [section])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`floating-select-toolbar-popup__section floating-select-toolbar-popup__section-${section.key}`}
|
||||
key={section.key}
|
||||
>
|
||||
{section.type === 'dropdown' &&
|
||||
section.entries.length &&
|
||||
(Icon ? (
|
||||
<React.Suspense>
|
||||
<ToolbarDropdown
|
||||
Icon={Icon}
|
||||
anchorElem={anchorElem}
|
||||
editor={editor}
|
||||
entries={section.entries}
|
||||
/>
|
||||
</React.Suspense>
|
||||
) : (
|
||||
<ToolbarDropdown anchorElem={anchorElem} editor={editor} entries={section.entries} />
|
||||
))}
|
||||
{section.type === 'buttons' &&
|
||||
section.entries.length &&
|
||||
section.entries.map((entry) => {
|
||||
return (
|
||||
<ButtonSectionEntry
|
||||
anchorElem={anchorElem}
|
||||
editor={editor}
|
||||
entry={entry}
|
||||
key={entry.key}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{index < editorConfig.features.floatingSelectToolbar?.sections.length - 1 && (
|
||||
<div className="divider" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FloatingSelectToolbar({
|
||||
anchorElem,
|
||||
editor,
|
||||
@@ -176,41 +289,13 @@ function FloatingSelectToolbar({
|
||||
{editorConfig?.features &&
|
||||
editorConfig.features?.floatingSelectToolbar?.sections.map((section, i) => {
|
||||
return (
|
||||
<div
|
||||
className={`floating-select-toolbar-popup__section floating-select-toolbar-popup__section-${section.key}`}
|
||||
<ToolbarSection
|
||||
anchorElem={anchorElem}
|
||||
editor={editor}
|
||||
index={i}
|
||||
key={section.key}
|
||||
>
|
||||
{section.type === 'dropdown' && section.entries.length && (
|
||||
<ToolbarDropdown
|
||||
Icon={section.ChildComponent}
|
||||
anchorElem={anchorElem}
|
||||
editor={editor}
|
||||
entries={section.entries}
|
||||
section={section}
|
||||
/>
|
||||
)}
|
||||
{section.type === 'buttons' &&
|
||||
section.entries.length &&
|
||||
section.entries.map((entry) => {
|
||||
if (entry.Component) {
|
||||
return (
|
||||
<entry.Component
|
||||
anchorElem={anchorElem}
|
||||
editor={editor}
|
||||
entry={entry}
|
||||
key={entry.key}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<ToolbarButton entry={entry} key={entry.key}>
|
||||
<entry.ChildComponent />
|
||||
</ToolbarButton>
|
||||
)
|
||||
})}
|
||||
{i < editorConfig.features.floatingSelectToolbar?.sections.length - 1 && (
|
||||
<div className="divider" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</React.Fragment>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { BaseSelection, LexicalEditor } from 'lexical'
|
||||
import type React from 'react'
|
||||
|
||||
export type FloatingToolbarSection =
|
||||
| {
|
||||
ChildComponent?: React.FC
|
||||
ChildComponent?: () => Promise<React.FC>
|
||||
entries: Array<FloatingToolbarSectionEntry>
|
||||
key: string
|
||||
order?: number
|
||||
@@ -16,13 +17,15 @@ export type FloatingToolbarSection =
|
||||
}
|
||||
|
||||
export type FloatingToolbarSectionEntry = {
|
||||
ChildComponent?: React.FC
|
||||
ChildComponent?: () => Promise<React.FC>
|
||||
/** Use component to ignore the children and onClick properties. It does not use the default, pre-defined format Button component */
|
||||
Component?: React.FC<{
|
||||
Component?: () => Promise<
|
||||
React.FC<{
|
||||
anchorElem: HTMLElement
|
||||
editor: LexicalEditor
|
||||
entry: FloatingToolbarSectionEntry
|
||||
}>
|
||||
>
|
||||
isActive?: ({ editor, selection }: { editor: LexicalEditor; selection: BaseSelection }) => boolean
|
||||
isEnabled?: ({
|
||||
editor,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { i18n } from 'i18next'
|
||||
import type { LexicalEditor } from 'lexical'
|
||||
import type { MutableRefObject } from 'react'
|
||||
import type React from 'react'
|
||||
|
||||
export class SlashMenuOption {
|
||||
// Icon for display
|
||||
Icon: React.FC
|
||||
Icon: () => Promise<React.FC>
|
||||
|
||||
displayName?: (({ i18n }: { i18n: i18n }) => string) | string
|
||||
// Used for class names and, if displayName is not provided, for display.
|
||||
@@ -21,7 +22,7 @@ export class SlashMenuOption {
|
||||
constructor(
|
||||
key: string,
|
||||
options: {
|
||||
Icon: React.FC
|
||||
Icon: () => Promise<React.FC>
|
||||
displayName?: (({ i18n }: { i18n: i18n }) => string) | string
|
||||
keyboardShortcut?: string
|
||||
keywords?: Array<string>
|
||||
|
||||
@@ -45,6 +45,16 @@ 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}
|
||||
@@ -58,7 +68,12 @@ function SlashMenuItem({
|
||||
tabIndex={-1}
|
||||
type="button"
|
||||
>
|
||||
<option.Icon />
|
||||
{LazyIcon && (
|
||||
<React.Suspense>
|
||||
<LazyIcon />
|
||||
</React.Suspense>
|
||||
)}
|
||||
|
||||
<span className={`${baseClass}__item-text`}>{title}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -2,17 +2,15 @@ import type { SerializedEditorState } from 'lexical'
|
||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||
import type { RichTextAdapter } from 'payload/types'
|
||||
|
||||
import { withMergedProps, withNullableJSONSchemaType } from 'payload/utilities'
|
||||
import { withNullableJSONSchemaType } from 'payload/utilities'
|
||||
|
||||
import type { FeatureProvider } from './field/features/types'
|
||||
import type { EditorConfig, SanitizedEditorConfig } from './field/lexical/config/types'
|
||||
import type { AdapterProps } from './types'
|
||||
|
||||
import { RichTextCell } from './cell'
|
||||
import { RichTextField } from './field'
|
||||
import {
|
||||
defaultEditorConfig,
|
||||
defaultEditorFeatures,
|
||||
defaultEditorLexicalConfig,
|
||||
defaultSanitizedEditorConfig,
|
||||
} from './field/lexical/config/default'
|
||||
import { sanitizeEditorConfig } from './field/lexical/config/sanitize'
|
||||
@@ -48,23 +46,38 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
features = cloneDeep(defaultEditorFeatures)
|
||||
}
|
||||
|
||||
const lexical: LexicalEditorConfig = props.lexical || cloneDeep(defaultEditorLexicalConfig)
|
||||
const lexical: LexicalEditorConfig = props.lexical
|
||||
|
||||
finalSanitizedEditorConfig = sanitizeEditorConfig({
|
||||
features,
|
||||
lexical,
|
||||
lexical: props.lexical ? () => Promise.resolve(lexical) : defaultEditorConfig.lexical,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
CellComponent: withMergedProps({
|
||||
LazyCellComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('./cell').then((module) => {
|
||||
const RichTextCell = module.RichTextCell
|
||||
return import('payload/utilities').then((module2) =>
|
||||
module2.withMergedProps({
|
||||
Component: RichTextCell,
|
||||
toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig },
|
||||
}),
|
||||
FieldComponent: withMergedProps({
|
||||
)
|
||||
}),
|
||||
|
||||
LazyFieldComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('./field').then((module) => {
|
||||
const RichTextField = module.RichTextField
|
||||
return import('payload/utilities').then((module2) =>
|
||||
module2.withMergedProps({
|
||||
Component: RichTextField,
|
||||
toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig },
|
||||
}),
|
||||
)
|
||||
}),
|
||||
afterReadPromise: ({ field, incomingEditorState, siblingDoc }) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const promises: Promise<void>[] = []
|
||||
@@ -307,15 +320,12 @@ export {
|
||||
export {
|
||||
defaultEditorConfig,
|
||||
defaultEditorFeatures,
|
||||
defaultEditorLexicalConfig,
|
||||
defaultSanitizedEditorConfig,
|
||||
} from './field/lexical/config/default'
|
||||
export { loadFeatures, sortFeaturesForOptimalLoading } from './field/lexical/config/loader'
|
||||
export { sanitizeEditorConfig, sanitizeFeatures } from './field/lexical/config/sanitize'
|
||||
export { getEnabledNodes } from './field/lexical/nodes'
|
||||
|
||||
export { ToolbarButton } from './field/lexical/plugins/FloatingSelectToolbar/ToolbarButton'
|
||||
export { ToolbarDropdown } from './field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/index'
|
||||
export {
|
||||
type FloatingToolbarSection,
|
||||
type FloatingToolbarSectionEntry,
|
||||
@@ -324,8 +334,7 @@ export { ENABLE_SLASH_MENU_COMMAND } from './field/lexical/plugins/SlashMenu/Lex
|
||||
// export SanitizedEditorConfig
|
||||
export type { EditorConfig, SanitizedEditorConfig }
|
||||
export type { AdapterProps }
|
||||
export { RichTextCell }
|
||||
export { RichTextField }
|
||||
|
||||
export {
|
||||
SlashMenuGroup,
|
||||
SlashMenuOption,
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const [baseDirRelativePath] = process.argv.slice(2)
|
||||
const [sourceDirRelativePath] = process.argv.slice(3)
|
||||
|
||||
// Base directory
|
||||
const baseDir = path.resolve(__dirname, '..')
|
||||
const sourceDir = path.join(baseDir, 'dist', 'exports')
|
||||
const baseDir = path.resolve(__dirname, baseDirRelativePath)
|
||||
const sourceDir = path.join(baseDir, sourceDirRelativePath)
|
||||
const targetDir = baseDir
|
||||
|
||||
// Helper function to read directories recursively and exclude .map files
|
||||
function getFiles (dir: string): string[] {
|
||||
function getFiles(dir: string): string[] {
|
||||
const subDirs = fs.readdirSync(dir, { withFileTypes: true })
|
||||
const files = subDirs.map((dirEntry) => {
|
||||
const res = path.resolve(dir, dirEntry.name)
|
||||
@@ -21,7 +24,7 @@ function getFiles (dir: string): string[] {
|
||||
return Array.prototype.concat(...files)
|
||||
}
|
||||
|
||||
function fixImports (fileExtension: string, content: string, depth: number): string {
|
||||
function fixImports(fileExtension: string, content: string, depth: number): string {
|
||||
const parentDirReference = '../'.repeat(depth + 1) // +1 to account for the original reference
|
||||
const replacementPrefix = (depth === 0 ? './' : '../'.repeat(depth)) + 'dist/'
|
||||
|
||||
Reference in New Issue
Block a user