Skip to content
Merged
Show file tree
Hide file tree
Changes from 74 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
4274182
progress
AlessioGr Sep 16, 2025
7924bc6
feat: support client-side block markdown transformers
AlessioGr Sep 16, 2025
4f86ea4
add slash command and markdown transformer e2e tests for code block
AlessioGr Sep 16, 2025
af7c265
more tests
AlessioGr Sep 16, 2025
e173d2c
docs
AlessioGr Sep 16, 2025
aad7d25
mark as experimental
AlessioGr Sep 16, 2025
a4c5f81
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Sep 24, 2025
266049e
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Sep 24, 2025
4a09cf6
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Sep 27, 2025
788c8fb
fix error
AlessioGr Sep 27, 2025
8c1ccac
improve naming
AlessioGr Sep 27, 2025
2281bf7
refactor: lexical block custom block and collapsible component props,…
AlessioGr Sep 27, 2025
1801169
fix edit button styling
AlessioGr Sep 27, 2025
446655c
add missing classname
AlessioGr Sep 27, 2025
6cf1394
feat: allows providing custom Pill component to lexical block compone…
AlessioGr Sep 27, 2025
5c9b439
feat: bring over CodeBlockIcon from figma design, replace existing Co…
AlessioGr Sep 27, 2025
d4b5fd4
feat: make more information available to BlockComponentContext
AlessioGr Sep 27, 2025
e196190
feat: code block: custom block component, initial design
AlessioGr Sep 27, 2025
da2e6a5
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Sep 29, 2025
487cfcb
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Sep 29, 2025
9049a7d
fix tests
AlessioGr Sep 29, 2025
0a319ae
refactor: cleanup block component eslint errors
AlessioGr Sep 29, 2025
732d9eb
feat: ability to override block actions
AlessioGr Sep 29, 2025
f5d32fd
feat: language picker
AlessioGr Sep 29, 2025
cc9d111
feat: copy to clipboard button
AlessioGr Sep 29, 2025
1b221b9
fix: ensure setting language marks form as modified
AlessioGr Sep 29, 2025
a5edf7e
feat: code block collapsing behavior
AlessioGr Sep 29, 2025
3355a4e
fix tests
AlessioGr Sep 29, 2025
e91df8c
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Oct 1, 2025
44f46b5
feat: allows overriding entire header
AlessioGr Oct 1, 2025
d68b36a
feat: allows disabling collapsible header toggle behavior
AlessioGr Oct 1, 2025
3ef9843
feat: new collapse button
AlessioGr Oct 1, 2025
e555b62
refactor: disable collapsible toggle button overlay completely if dis…
AlessioGr Oct 1, 2025
961969d
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Oct 1, 2025
9d3fc43
override monaco editor theme to match block header color
AlessioGr Oct 1, 2025
7b8d217
fix icon colors
AlessioGr Oct 1, 2025
128345e
feat: floating collapse button
AlessioGr Oct 1, 2025
56a1b3b
label
AlessioGr Oct 1, 2025
df886cf
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Oct 1, 2025
898dbd3
add remove block button
AlessioGr Oct 1, 2025
d8b427e
support jsx, disable squiggly lines by default
AlessioGr Oct 1, 2025
df4793d
feat: allow passing through editor props
AlessioGr Oct 1, 2025
062046e
feat: jsx support
AlessioGr Oct 1, 2025
58ab390
add docs
AlessioGr Oct 1, 2025
5d79493
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Oct 1, 2025
cb85185
fix readonly handling
AlessioGr Oct 2, 2025
deb0686
do not render InsertParagraphAtEndPlugin and OnChangePlugin if editor…
AlessioGr Oct 2, 2025
917c03d
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Oct 2, 2025
7a8ba17
export stuff
AlessioGr Oct 2, 2025
152bf35
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Oct 2, 2025
5ab25cf
export stuff
AlessioGr Oct 3, 2025
6b23c29
export more
AlessioGr Oct 3, 2025
b804ca6
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Oct 3, 2025
3d25465
export more stuff
AlessioGr Oct 3, 2025
148afe0
feat: pass more props to dashboard view
AlessioGr Oct 3, 2025
7dedb09
perf: use select api, improve types
AlessioGr Oct 3, 2025
ff6711a
add missing props
AlessioGr Oct 3, 2025
b78b95f
fix incorrect type
AlessioGr Oct 4, 2025
88ce4e9
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Oct 4, 2025
e8ce261
extract next changes
AlessioGr Oct 6, 2025
15aa3c7
dedupe
AlessioGr Oct 6, 2025
c83e331
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Oct 6, 2025
9600e0a
further simplify
AlessioGr Oct 6, 2025
e981770
allows overriding height
AlessioGr Oct 6, 2025
2be3080
Revert "further simplify"
AlessioGr Oct 6, 2025
4d6e3fe
feat: support all languages by default
AlessioGr Oct 7, 2025
0da0ef2
remove unnecessary fragment
AlessioGr Oct 7, 2025
93a850c
Merge remote-tracking branch 'origin/main' into feat/code-block
AlessioGr Oct 7, 2025
01ef157
max height for popup container
AlessioGr Oct 7, 2025
c8a5586
style: ensure area in between popup buttons is clickable
AlessioGr Oct 7, 2025
86a4276
feat: ComboBox
AlessioGr Oct 7, 2025
efcc3d1
add plaintext
AlessioGr Oct 7, 2025
246b612
add source for default languages
AlessioGr Oct 7, 2025
c8403a3
i18n
AlessioGr Oct 7, 2025
11d4c70
fix test
AlessioGr Oct 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions docs/rich-text/official-features.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,69 @@ BlocksFeature({
})
```

#### Code Blocks

Payload exports a premade CodeBlock that you can import and use in your project. It supports syntax highlighting, dynamically selecting the language and loading in external type definitions:

```ts
import { BlocksFeature, CodeBlock } from '@payloadcms/richtext-lexical'

// ...
BlocksFeature({
blocks: [
CodeBlock({
defaultLanguage: 'ts',
languages: {
js: 'JavaScript',
plaintext: 'Plain Text',
ts: 'TypeScript',
Comment on lines +433 to +436
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why define supported languages ​​when they could all be included?
I thought it might be a bundle thing or something, but they already come with Monaco for the same price, right?
In case it's a matter of making the dropdown menu not so long, I would simply do it with limited height + scroll and search (Combobox)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Screenshot.2025-10-06.at.20.01.07.mp4

},
}),
],
}),
// ...
```

When using TypeScript, you can also pass in additional type definitions that will be available in the editor. Here's an example of how to make `payload` and `react` available in the editor:

```ts
import { BlocksFeature, CodeBlock } from '@payloadcms/richtext-lexical'

// ...
BlocksFeature({
blocks: [
CodeBlock({
slug: 'PayloadCode',
languages: {
ts: 'TypeScript',
},
typescript: {
fetchTypes: [
{
// The index.bundled.d.ts contains all the types for Payload in one file, so that Monaco doesn't need to fetch multiple files.
// This file may be removed in the future and is not guaranteed to be available in future versions of Payload.
url: 'https://unpkg.com/[email protected]/dist/index.bundled.d.ts',
filePath: 'file:///node_modules/payload/index.d.ts',
},
{
url: 'https://unpkg.com/@types/[email protected]/index.d.ts',
filePath: 'file:///node_modules/@types/react/index.d.ts',
},
],
paths: {
payload: ['file:///node_modules/payload/index.d.ts'],
react: ['file:///node_modules/@types/react/index.d.ts'],
},
typeRoots: ['node_modules/@types', 'node_modules/payload'],
// Enable type checking. By default, only syntax checking is enabled.
enableSemanticValidation: true,
},
}),
],
}),
// ...
```

### TreeViewFeature

- Description: Provides a debug panel below the editor showing the editor's internal state, DOM tree, and time travel debugging.
Expand Down
6 changes: 4 additions & 2 deletions packages/payload/src/fields/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,7 @@ export type CodeField = {
Label?: CustomComponent<CodeFieldLabelClientComponent | CodeFieldLabelServerComponent>
} & Admin['components']
editorOptions?: EditorProps['options']
editorProps?: Partial<EditorProps>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

editorProps is from Monaco, right? Don't we want to exclude some properties like height?

Copy link
Member Author

@AlessioGr AlessioGr Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

editorProps is from Monaco, right?

Monaco + some extra properties.

Don't we want to exclude some properties like height?

You're right! The previous type was inaccurate because the height property was effectively ignored. I fixed this by moving our height property override up, before we spread the props - the type should be accurate now! Few benefits over just excluding the property:

  • The type is simpler and easier to read
  • Overriding the height may be useful for some. More flexible

language?: string
} & Admin
maxLength?: number
Expand All @@ -1070,8 +1071,9 @@ export type CodeField = {
} & Omit<FieldBase, 'admin' | 'validate'>

export type CodeFieldClient = {
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
admin?: AdminClient & Pick<CodeField['admin'], 'editorOptions' | 'language'>
admin?: AdminClient &
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
Partial<Pick<CodeField['admin'], 'editorOptions' | 'editorProps' | 'language'>>
} & Omit<FieldBaseClient, 'admin'> &
Pick<CodeField, 'maxLength' | 'minLength' | 'type'>

Expand Down
3 changes: 3 additions & 0 deletions packages/richtext-lexical/src/exports/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ export { BlockEditButton } from '../../features/blocks/client/component/componen
export { BlockRemoveButton } from '../../features/blocks/client/component/components/BlockRemoveButton.js'
export { useBlockComponentContext } from '../../features/blocks/client/component/BlockContent.js'
export { getRestPopulateFn } from '../../features/converters/utilities/restPopulateFn.js'
export { codeConverterClient } from '../../features/blocks/premade/CodeBlock/converterClient.js'
export { CodeComponent } from '../../features/blocks/premade/CodeBlock/Component/Code.js'
export { CodeBlockBlockComponent } from '../../features/blocks/premade/CodeBlock/Component/Block.js'

export { RenderLexical } from '../../field/RenderLexical/index.js'
export { buildEditorState } from '../../utilities/buildEditorState.js'
Original file line number Diff line number Diff line change
@@ -1,23 +1,63 @@
'use client'
import type { CollapsibleProps } from '@payloadcms/ui/elements/Collapsible'
import type { ClientField, FormState } from 'payload'

import { useLexicalEditable } from '@lexical/react/useLexicalEditable'
import { RenderFields, useFormSubmitted } from '@payloadcms/ui'
import React, { createContext, useMemo } from 'react'

type Props = {
export type BlockCollapsibleProps = {
/**
* Replace the top-right portion of the header that renders the Edit and Remove buttons with custom content.
* If this property is provided, the `removeButton` and `editButton` properties are ignored.
*/
Actions?: React.ReactNode
children?: React.ReactNode
/**
* Additional className to the collapsible wrapper
*/
className?: string
/**
* Props to pass to the underlying Collapsible component. You could use this to override the `Header` entirely, for example.
*/
collapsibleProps?: Partial<CollapsibleProps>
/**
* Whether to disable rendering the block name field in the header Label
* @default false
*/
disableBlockName?: boolean
/**
* Whether to show the Edit button
* If `Actions` is provided, this property is ignored.
* @default true
*/
editButton?: boolean
/**
* Replace the default Label component with a custom Label
*/
Label?: React.ReactNode
/**
* Replace the default Pill component component that's rendered within the default Label component with a custom Pill.
* This property has no effect if you provide a custom Label component via the `Label` property.
*/
Pill?: React.ReactNode
/**
* Whether to show the Remove button
* If `Actions` is provided, this property is ignored.
* @default true
*/
removeButton?: boolean
}

export type BlockCollapsibleWithErrorProps = {
errorCount?: number
fieldHasErrors?: boolean
} & BlockCollapsibleProps

export type BlockContentProps = {
baseClass: string
BlockDrawer: React.FC
Collapsible: React.FC<{
children?: React.ReactNode
editButton?: boolean
errorCount?: number
fieldHasErrors?: boolean
/**
* Override the default label with a custom label
*/
Label?: React.ReactNode
removeButton?: boolean
}>
Collapsible: React.FC<BlockCollapsibleWithErrorProps>
CustomBlock: React.ReactNode
EditButton: React.FC
errorCount: number
Expand All @@ -29,24 +69,20 @@ type Props = {
}

type BlockComponentContextType = {
BlockCollapsible?: React.FC<{
children?: React.ReactNode
editButton?: boolean
/**
* Override the default label with a custom label
*/
Label?: React.ReactNode
removeButton?: boolean
}>
EditButton?: React.FC
initialState: false | FormState | undefined

nodeKey?: string
RemoveButton?: React.FC
}
BlockCollapsible: React.FC<BlockCollapsibleProps>
} & Omit<BlockContentProps, 'Collapsible'>

const BlockComponentContext = createContext<BlockComponentContextType>({
baseClass: 'lexical-block',
BlockCollapsible: () => null,
BlockDrawer: () => null,
CustomBlock: null,
EditButton: () => null,
errorCount: 0,
formSchema: [],
initialState: false,
nodeKey: '',
RemoveButton: () => null,
})

export const useBlockComponentContext = () => React.use(BlockComponentContext)
Expand All @@ -56,56 +92,33 @@ export const useBlockComponentContext = () => React.use(BlockComponentContext)
* scoped to the block. All format operations in here are thus scoped to the block's form, and
* not the whole document.
*/
export const BlockContent: React.FC<Props> = (props) => {
const {
BlockDrawer,
Collapsible,
CustomBlock,
EditButton,
errorCount,
formSchema,
initialState,
nodeKey,
RemoveButton,
} = props
export const BlockContent: React.FC<BlockContentProps> = (props) => {
const { Collapsible, ...contextProps } = props

const { BlockDrawer, CustomBlock, errorCount, formSchema } = contextProps

const hasSubmitted = useFormSubmitted()

const fieldHasErrors = hasSubmitted && errorCount > 0
const isEditable = useLexicalEditable()

const CollapsibleWithErrorProps = useMemo(
() =>
(props: {
children?: React.ReactNode
editButton?: boolean

/**
* Override the default label with a custom label
*/
Label?: React.ReactNode
removeButton?: boolean
}) => (
<Collapsible
editButton={props.editButton}
errorCount={errorCount}
fieldHasErrors={fieldHasErrors}
Label={props.Label}
removeButton={props.removeButton}
>
{props.children}
() => (props: BlockCollapsibleProps) => {
const { children, ...rest } = props
return (
<Collapsible errorCount={errorCount} fieldHasErrors={fieldHasErrors} {...rest}>
{children}
</Collapsible>
),
)
},
[Collapsible, fieldHasErrors, errorCount],
)

return CustomBlock ? (
<BlockComponentContext
value={{
...contextProps,
BlockCollapsible: CollapsibleWithErrorProps,
EditButton,
initialState,
nodeKey,
RemoveButton,
}}
>
{CustomBlock}
Expand All @@ -120,6 +133,7 @@ export const BlockContent: React.FC<Props> = (props) => {
parentPath={''}
parentSchemaPath=""
permissions={true}
readOnly={!isEditable}
/>
</CollapsibleWithErrorProps>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
'use client'
import React from 'react'

import { useBlockComponentContext } from '../BlockContent.js'
import { type BlockCollapsibleProps, useBlockComponentContext } from '../BlockContent.js'

export const BlockCollapsible: React.FC<{
children?: React.ReactNode
editButton?: boolean

/**
* Override the default label with a custom label
*/
Label?: React.ReactNode
removeButton?: boolean
}> = ({ children, editButton, Label, removeButton }) => {
export const BlockCollapsible: React.FC<BlockCollapsibleProps> = (props) => {
const { children, ...rest } = props
const { BlockCollapsible } = useBlockComponentContext()

return BlockCollapsible ? (
<BlockCollapsible editButton={editButton} Label={Label} removeButton={removeButton}>
{children}
</BlockCollapsible>
) : null
return BlockCollapsible ? <BlockCollapsible {...rest}>{children}</BlockCollapsible> : null
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@

&__editButton.btn {
margin: 0;
width: 24px;

&:hover {
background-color: var(--theme-elevation-200);
}
Expand Down
Loading
Loading