Skip to content

Commit 07a1eff

Browse files
authored
feat(richtext-lexical): client-side block markdown shortcuts, code block (#13813)
This PR: - adds support for client-side markdown shortcuts for blocks with the `admin.jsx` property. This was previously only supported server-side. The code mimics the server-side implementation, as this could be used for full markdown imports on the client-side, rather than just simple markdown shortcuts - exports a pre-made `CodeBlock` that can be used within the `BlocksFeature`. Video Example: https://github.com/user-attachments/assets/31f2d66b-64e9-4e9d-a2b6-89ddefda3a84 --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211217132290839
1 parent fcaafaa commit 07a1eff

File tree

105 files changed

+2034
-547
lines changed

Some content is hidden

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

105 files changed

+2034
-547
lines changed

docs/rich-text/official-features.mdx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,69 @@ BlocksFeature({
418418
})
419419
```
420420
421+
#### Code Blocks
422+
423+
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:
424+
425+
```ts
426+
import { BlocksFeature, CodeBlock } from '@payloadcms/richtext-lexical'
427+
428+
// ...
429+
BlocksFeature({
430+
blocks: [
431+
CodeBlock({
432+
defaultLanguage: 'ts',
433+
languages: {
434+
js: 'JavaScript',
435+
plaintext: 'Plain Text',
436+
ts: 'TypeScript',
437+
},
438+
}),
439+
],
440+
}),
441+
// ...
442+
```
443+
444+
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:
445+
446+
```ts
447+
import { BlocksFeature, CodeBlock } from '@payloadcms/richtext-lexical'
448+
449+
// ...
450+
BlocksFeature({
451+
blocks: [
452+
CodeBlock({
453+
slug: 'PayloadCode',
454+
languages: {
455+
ts: 'TypeScript',
456+
},
457+
typescript: {
458+
fetchTypes: [
459+
{
460+
// The index.bundled.d.ts contains all the types for Payload in one file, so that Monaco doesn't need to fetch multiple files.
461+
// This file may be removed in the future and is not guaranteed to be available in future versions of Payload.
462+
url: 'https://unpkg.com/[email protected]/dist/index.bundled.d.ts',
463+
filePath: 'file:///node_modules/payload/index.d.ts',
464+
},
465+
{
466+
url: 'https://unpkg.com/@types/[email protected]/index.d.ts',
467+
filePath: 'file:///node_modules/@types/react/index.d.ts',
468+
},
469+
],
470+
paths: {
471+
payload: ['file:///node_modules/payload/index.d.ts'],
472+
react: ['file:///node_modules/@types/react/index.d.ts'],
473+
},
474+
typeRoots: ['node_modules/@types', 'node_modules/payload'],
475+
// Enable type checking. By default, only syntax checking is enabled.
476+
enableSemanticValidation: true,
477+
},
478+
}),
479+
],
480+
}),
481+
// ...
482+
```
483+
421484
### TreeViewFeature
422485

423486
- Description: Provides a debug panel below the editor showing the editor's internal state, DOM tree, and time travel debugging.

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,7 @@ export type CodeField = {
10611061
Label?: CustomComponent<CodeFieldLabelClientComponent | CodeFieldLabelServerComponent>
10621062
} & FieldAdmin['components']
10631063
editorOptions?: EditorProps['options']
1064+
editorProps?: Partial<EditorProps>
10641065
language?: string
10651066
} & FieldAdmin
10661067
maxLength?: number
@@ -1070,8 +1071,9 @@ export type CodeField = {
10701071
} & Omit<FieldBase, 'admin' | 'validate'>
10711072

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

packages/richtext-lexical/src/exports/client/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ export { BlockEditButton } from '../../features/blocks/client/component/componen
150150
export { BlockRemoveButton } from '../../features/blocks/client/component/components/BlockRemoveButton.js'
151151
export { useBlockComponentContext } from '../../features/blocks/client/component/BlockContent.js'
152152
export { getRestPopulateFn } from '../../features/converters/utilities/restPopulateFn.js'
153+
export { codeConverterClient } from '../../features/blocks/premade/CodeBlock/converterClient.js'
154+
export { CodeComponent } from '../../features/blocks/premade/CodeBlock/Component/Code.js'
155+
export { CodeBlockBlockComponent } from '../../features/blocks/premade/CodeBlock/Component/Block.js'
153156

154157
export { RenderLexical } from '../../field/RenderLexical/index.js'
155158
export { buildEditorState } from '../../utilities/buildEditorState.js'

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

Lines changed: 77 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,63 @@
11
'use client'
2+
import type { CollapsibleProps } from '@payloadcms/ui/elements/Collapsible'
23
import type { ClientField, FormState } from 'payload'
34

5+
import { useLexicalEditable } from '@lexical/react/useLexicalEditable'
46
import { RenderFields, useFormSubmitted } from '@payloadcms/ui'
57
import React, { createContext, useMemo } from 'react'
68

7-
type Props = {
9+
export type BlockCollapsibleProps = {
10+
/**
11+
* Replace the top-right portion of the header that renders the Edit and Remove buttons with custom content.
12+
* If this property is provided, the `removeButton` and `editButton` properties are ignored.
13+
*/
14+
Actions?: React.ReactNode
15+
children?: React.ReactNode
16+
/**
17+
* Additional className to the collapsible wrapper
18+
*/
19+
className?: string
20+
/**
21+
* Props to pass to the underlying Collapsible component. You could use this to override the `Header` entirely, for example.
22+
*/
23+
collapsibleProps?: Partial<CollapsibleProps>
24+
/**
25+
* Whether to disable rendering the block name field in the header Label
26+
* @default false
27+
*/
28+
disableBlockName?: boolean
29+
/**
30+
* Whether to show the Edit button
31+
* If `Actions` is provided, this property is ignored.
32+
* @default true
33+
*/
34+
editButton?: boolean
35+
/**
36+
* Replace the default Label component with a custom Label
37+
*/
38+
Label?: React.ReactNode
39+
/**
40+
* Replace the default Pill component component that's rendered within the default Label component with a custom Pill.
41+
* This property has no effect if you provide a custom Label component via the `Label` property.
42+
*/
43+
Pill?: React.ReactNode
44+
/**
45+
* Whether to show the Remove button
46+
* If `Actions` is provided, this property is ignored.
47+
* @default true
48+
*/
49+
removeButton?: boolean
50+
}
51+
52+
export type BlockCollapsibleWithErrorProps = {
53+
errorCount?: number
54+
fieldHasErrors?: boolean
55+
} & BlockCollapsibleProps
56+
57+
export type BlockContentProps = {
858
baseClass: string
959
BlockDrawer: React.FC
10-
Collapsible: React.FC<{
11-
children?: React.ReactNode
12-
editButton?: boolean
13-
errorCount?: number
14-
fieldHasErrors?: boolean
15-
/**
16-
* Override the default label with a custom label
17-
*/
18-
Label?: React.ReactNode
19-
removeButton?: boolean
20-
}>
60+
Collapsible: React.FC<BlockCollapsibleWithErrorProps>
2161
CustomBlock: React.ReactNode
2262
EditButton: React.FC
2363
errorCount: number
@@ -29,24 +69,20 @@ type Props = {
2969
}
3070

3171
type BlockComponentContextType = {
32-
BlockCollapsible?: React.FC<{
33-
children?: React.ReactNode
34-
editButton?: boolean
35-
/**
36-
* Override the default label with a custom label
37-
*/
38-
Label?: React.ReactNode
39-
removeButton?: boolean
40-
}>
41-
EditButton?: React.FC
42-
initialState: false | FormState | undefined
43-
44-
nodeKey?: string
45-
RemoveButton?: React.FC
46-
}
72+
BlockCollapsible: React.FC<BlockCollapsibleProps>
73+
} & Omit<BlockContentProps, 'Collapsible'>
4774

4875
const BlockComponentContext = createContext<BlockComponentContextType>({
76+
baseClass: 'lexical-block',
77+
BlockCollapsible: () => null,
78+
BlockDrawer: () => null,
79+
CustomBlock: null,
80+
EditButton: () => null,
81+
errorCount: 0,
82+
formSchema: [],
4983
initialState: false,
84+
nodeKey: '',
85+
RemoveButton: () => null,
5086
})
5187

5288
export const useBlockComponentContext = () => React.use(BlockComponentContext)
@@ -56,56 +92,33 @@ export const useBlockComponentContext = () => React.use(BlockComponentContext)
5692
* scoped to the block. All format operations in here are thus scoped to the block's form, and
5793
* not the whole document.
5894
*/
59-
export const BlockContent: React.FC<Props> = (props) => {
60-
const {
61-
BlockDrawer,
62-
Collapsible,
63-
CustomBlock,
64-
EditButton,
65-
errorCount,
66-
formSchema,
67-
initialState,
68-
nodeKey,
69-
RemoveButton,
70-
} = props
95+
export const BlockContent: React.FC<BlockContentProps> = (props) => {
96+
const { Collapsible, ...contextProps } = props
97+
98+
const { BlockDrawer, CustomBlock, errorCount, formSchema } = contextProps
7199

72100
const hasSubmitted = useFormSubmitted()
73101

74102
const fieldHasErrors = hasSubmitted && errorCount > 0
103+
const isEditable = useLexicalEditable()
75104

76105
const CollapsibleWithErrorProps = useMemo(
77-
() =>
78-
(props: {
79-
children?: React.ReactNode
80-
editButton?: boolean
81-
82-
/**
83-
* Override the default label with a custom label
84-
*/
85-
Label?: React.ReactNode
86-
removeButton?: boolean
87-
}) => (
88-
<Collapsible
89-
editButton={props.editButton}
90-
errorCount={errorCount}
91-
fieldHasErrors={fieldHasErrors}
92-
Label={props.Label}
93-
removeButton={props.removeButton}
94-
>
95-
{props.children}
106+
() => (props: BlockCollapsibleProps) => {
107+
const { children, ...rest } = props
108+
return (
109+
<Collapsible errorCount={errorCount} fieldHasErrors={fieldHasErrors} {...rest}>
110+
{children}
96111
</Collapsible>
97-
),
112+
)
113+
},
98114
[Collapsible, fieldHasErrors, errorCount],
99115
)
100116

101117
return CustomBlock ? (
102118
<BlockComponentContext
103119
value={{
120+
...contextProps,
104121
BlockCollapsible: CollapsibleWithErrorProps,
105-
EditButton,
106-
initialState,
107-
nodeKey,
108-
RemoveButton,
109122
}}
110123
>
111124
{CustomBlock}
@@ -120,6 +133,7 @@ export const BlockContent: React.FC<Props> = (props) => {
120133
parentPath={''}
121134
parentSchemaPath=""
122135
permissions={true}
136+
readOnly={!isEditable}
123137
/>
124138
</CollapsibleWithErrorProps>
125139
)
Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,11 @@
11
'use client'
22
import React from 'react'
33

4-
import { useBlockComponentContext } from '../BlockContent.js'
4+
import { type BlockCollapsibleProps, useBlockComponentContext } from '../BlockContent.js'
55

6-
export const BlockCollapsible: React.FC<{
7-
children?: React.ReactNode
8-
editButton?: boolean
9-
10-
/**
11-
* Override the default label with a custom label
12-
*/
13-
Label?: React.ReactNode
14-
removeButton?: boolean
15-
}> = ({ children, editButton, Label, removeButton }) => {
6+
export const BlockCollapsible: React.FC<BlockCollapsibleProps> = (props) => {
7+
const { children, ...rest } = props
168
const { BlockCollapsible } = useBlockComponentContext()
179

18-
return BlockCollapsible ? (
19-
<BlockCollapsible editButton={editButton} Label={Label} removeButton={removeButton}>
20-
{children}
21-
</BlockCollapsible>
22-
) : null
10+
return BlockCollapsible ? <BlockCollapsible {...rest}>{children}</BlockCollapsible> : null
2311
}

packages/richtext-lexical/src/features/blocks/client/component/index.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@
9898

9999
&__editButton.btn {
100100
margin: 0;
101+
width: 24px;
102+
101103
&:hover {
102104
background-color: var(--theme-elevation-200);
103105
}

0 commit comments

Comments
 (0)