Skip to content

Commit 676dfa3

Browse files
authored
feat(richtext-lexical): inline blocks (#7102)
1 parent 1ea2e32 commit 676dfa3

File tree

41 files changed

+1114
-328
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1114
-328
lines changed

packages/payload/src/fields/config/schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,9 @@ export const blocks = baseField.keys({
449449
joi.object({
450450
slug: joi.string().required(),
451451
admin: joi.object().keys({
452+
components: joi.object().keys({
453+
Label: componentSchema,
454+
}),
452455
custom: joi.object().pattern(joi.string(), joi.any()),
453456
}),
454457
custom: joi.object().pattern(joi.string(), joi.any()),

packages/payload/src/fields/config/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,15 @@ export type RadioField = {
727727

728728
export type Block = {
729729
admin?: {
730+
components?: {
731+
Label?: React.FC<{
732+
blockKind: 'block' | 'lexicalBlock' | 'lexicalInlineBlock' | string
733+
/**
734+
* May contain the formData
735+
*/
736+
formData: Record<string, any>
737+
}>
738+
}
730739
/** Extension point to add your custom data. Available in server and client. */
731740
custom?: Record<string, any>
732741
}

packages/richtext-lexical/src/features/blocks/component/BlockContent.tsx

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { FormFieldBase } from '@payloadcms/ui'
2-
import type { FieldMap, ReducedBlock } from '@payloadcms/ui/utilities/buildComponentMap'
2+
import type { FieldMap } from '@payloadcms/ui/utilities/buildComponentMap'
33
import type { CollapsedPreferences, Data, FormState } from 'payload'
44

55
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js'
@@ -20,6 +20,7 @@ import { $getNodeByKey } from 'lexical'
2020
import React, { useCallback } from 'react'
2121

2222
import type { SanitizedClientEditorConfig } from '../../../lexical/config/types.js'
23+
import type { ClientBlock } from '../feature.client.js'
2324
import type { BlockFields, BlockNode } from '../nodes/BlocksNode.js'
2425

2526
import { FormSavePlugin } from './FormSavePlugin.js'
@@ -35,7 +36,7 @@ type Props = {
3536
formSchema: FieldMap
3637
nodeKey: string
3738
path: string
38-
reducedBlock: ReducedBlock
39+
reducedBlock: ClientBlock
3940
schemaPath: string
4041
}
4142

@@ -51,7 +52,7 @@ export const BlockContent: React.FC<Props> = (props) => {
5152
formData,
5253
formSchema,
5354
nodeKey,
54-
reducedBlock: { labels },
55+
reducedBlock: { LabelComponent, labels },
5556
schemaPath,
5657
} = props
5758

@@ -198,34 +199,38 @@ export const BlockContent: React.FC<Props> = (props) => {
198199
className={classNames}
199200
collapsibleStyle={fieldHasErrors ? 'error' : 'default'}
200201
header={
201-
<div className={`${baseClass}__block-header`}>
202-
<div>
203-
<Pill
204-
className={`${baseClass}__block-pill ${baseClass}__block-pill-${formData?.blockType}`}
205-
pillStyle="white"
206-
>
207-
{typeof labels.singular === 'string'
208-
? getTranslation(labels.singular, i18n)
209-
: '[Singular Label]'}
210-
</Pill>
211-
<SectionTitle path="blockName" readOnly={field?.readOnly} />
212-
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
202+
LabelComponent ? (
203+
<LabelComponent blockKind={'lexicalBlock'} formData={formData} />
204+
) : (
205+
<div className={`${baseClass}__block-header`}>
206+
<div>
207+
<Pill
208+
className={`${baseClass}__block-pill ${baseClass}__block-pill-${formData?.blockType}`}
209+
pillStyle="white"
210+
>
211+
{typeof labels.singular === 'string'
212+
? getTranslation(labels.singular, i18n)
213+
: '[Singular Label]'}
214+
</Pill>
215+
<SectionTitle path="blockName" readOnly={field?.readOnly} />
216+
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
217+
</div>
218+
{editor.isEditable() && (
219+
<Button
220+
buttonStyle="icon-label"
221+
className={`${baseClass}__removeButton`}
222+
disabled={field?.readOnly}
223+
icon="x"
224+
onClick={(e) => {
225+
e.preventDefault()
226+
removeBlock()
227+
}}
228+
round
229+
tooltip="Remove Block"
230+
/>
231+
)}
213232
</div>
214-
{editor.isEditable() && (
215-
<Button
216-
buttonStyle="icon-label"
217-
className={`${baseClass}__removeButton`}
218-
disabled={field?.readOnly}
219-
icon="x"
220-
onClick={(e) => {
221-
e.preventDefault()
222-
removeBlock()
223-
}}
224-
round
225-
tooltip="Remove Block"
226-
/>
227-
)}
228-
</div>
233+
)
229234
}
230235
isCollapsed={isCollapsed}
231236
key={0}

packages/richtext-lexical/src/features/blocks/component/index.tsx

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,23 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
1818

1919
import { type BlockFields } from '../nodes/BlocksNode.js'
2020
const baseClass = 'lexical-block'
21-
import type { ReducedBlock } from '@payloadcms/ui/utilities/buildComponentMap'
2221
import type { FormState } from 'payload'
2322

2423
import { getTranslation } from '@payloadcms/translations'
2524
import { getFormState } from '@payloadcms/ui/shared'
2625
import { v4 as uuid } from 'uuid'
2726

2827
import type { ClientComponentProps } from '../../typesClient.js'
29-
import type { BlocksFeatureClientProps } from '../feature.client.js'
28+
import type { BlocksFeatureClientProps, ClientBlock } from '../feature.client.js'
3029

3130
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider.js'
3231
import { BlockContent } from './BlockContent.js'
3332
import './index.scss'
3433

3534
type Props = {
3635
children?: React.ReactNode
37-
3836
formData: BlockFields
3937
nodeKey?: string
40-
/**
41-
* This transformedFormData already comes wrapped in blockFieldWrapperName
42-
*/
43-
transformedFormData: BlockFields
4438
}
4539

4640
export const BlockComponent: React.FC<Props> = (props) => {
@@ -59,7 +53,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
5953
const componentMapRenderedFieldsPath = `lexical_internal_feature.blocks.fields.${formData?.blockType}`
6054
const schemaFieldsPath = `${schemaPath}.lexical_internal_feature.blocks.${formData?.blockType}`
6155

62-
const reducedBlock: ReducedBlock = (
56+
const reducedBlock: ClientBlock = (
6357
editorConfig?.resolvedFeatureMap?.get('blocks')
6458
?.sanitizedClientFeatureProps as ClientComponentProps<BlocksFeatureClientProps>
6559
)?.reducedBlocks?.find((block) => block.slug === formData?.blockType)
@@ -127,6 +121,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
127121

128122
const classNames = [`${baseClass}__row`, `${baseClass}__row--no-errors`].filter(Boolean).join(' ')
129123

124+
const LabelComponent = reducedBlock?.LabelComponent
125+
130126
// Memoized Form JSX
131127
const formContent = useMemo(() => {
132128
return reducedBlock && initialState !== false ? (
@@ -155,19 +151,23 @@ export const BlockComponent: React.FC<Props> = (props) => {
155151
className={classNames}
156152
collapsibleStyle="default"
157153
header={
158-
<div className={`${baseClass}__block-header`}>
159-
<div>
160-
<Pill
161-
className={`${baseClass}__block-pill ${baseClass}__block-pill-${formData?.blockType}`}
162-
pillStyle="white"
163-
>
164-
{typeof reducedBlock.labels.singular === 'string'
165-
? getTranslation(reducedBlock.labels.singular, i18n)
166-
: '[Singular Label]'}
167-
</Pill>
168-
<SectionTitle path="blockName" readOnly={parentLexicalRichTextField?.readOnly} />
154+
LabelComponent ? (
155+
<LabelComponent blockKind={'lexicalBlock'} formData={formData} />
156+
) : (
157+
<div className={`${baseClass}__block-header`}>
158+
<div>
159+
<Pill
160+
className={`${baseClass}__block-pill ${baseClass}__block-pill-${formData?.blockType}`}
161+
pillStyle="white"
162+
>
163+
{reducedBlock && typeof reducedBlock.labels.singular === 'string'
164+
? getTranslation(reducedBlock.labels.singular, i18n)
165+
: '[Singular Label]'}
166+
</Pill>
167+
<SectionTitle path="blockName" readOnly={parentLexicalRichTextField?.readOnly} />
168+
</div>
169169
</div>
170-
</div>
170+
)
171171
}
172172
key={0}
173173
>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
@import '../../../scss/styles.scss';
2+
3+
.inline-block-container {
4+
display: inline-block;
5+
}
6+
7+
.inline-block {
8+
@extend %body;
9+
@include shadow-sm;
10+
padding: calc(var(--base) * 0.125) calc(var(--base) * 0.125) calc(var(--base) * 0.125) calc(var(--base) * 0.3);
11+
display: flex;
12+
align-items: center;
13+
background: var(--theme-input-bg);
14+
border: 1px solid var(--theme-elevation-100);
15+
border-radius: $style-radius-m;
16+
max-width: calc(var(--base) * 15);
17+
font-family: var(--font-body);
18+
margin-right: $style-stroke-width-m;
19+
margin-left: $style-stroke-width-m;
20+
21+
&:hover {
22+
border: 1px solid var(--theme-elevation-150);
23+
}
24+
25+
&__wrap {
26+
flex-grow: 1;
27+
overflow: hidden;
28+
}
29+
30+
&--selected {
31+
box-shadow: $focus-box-shadow;
32+
outline: none;
33+
}
34+
35+
&__editButton.btn {
36+
margin: 0;
37+
}
38+
39+
&__editButton {
40+
&:disabled {
41+
color: var(--theme-elevation-300);
42+
pointer-events: none;
43+
}
44+
}
45+
46+
&__actions {
47+
display: flex;
48+
align-items: center;
49+
flex-shrink: 0;
50+
margin-left: calc(var(--base) * 0.15);
51+
52+
53+
54+
svg {
55+
width: 20px;
56+
height: 20px;
57+
}
58+
}
59+
60+
&__removeButton.btn {
61+
margin: 0;
62+
63+
line {
64+
stroke-width: $style-stroke-width-m;
65+
}
66+
67+
&:disabled {
68+
color: var(--theme-elevation-300);
69+
pointer-events: none;
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)