feat(richtext-lexical): inline blocks (#7102)
This commit is contained in:
@@ -449,6 +449,9 @@ export const blocks = baseField.keys({
|
||||
joi.object({
|
||||
slug: joi.string().required(),
|
||||
admin: joi.object().keys({
|
||||
components: joi.object().keys({
|
||||
Label: componentSchema,
|
||||
}),
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
}),
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
|
||||
@@ -727,6 +727,15 @@ export type RadioField = {
|
||||
|
||||
export type Block = {
|
||||
admin?: {
|
||||
components?: {
|
||||
Label?: React.FC<{
|
||||
blockKind: 'block' | 'lexicalBlock' | 'lexicalInlineBlock' | string
|
||||
/**
|
||||
* May contain the formData
|
||||
*/
|
||||
formData: Record<string, any>
|
||||
}>
|
||||
}
|
||||
/** Extension point to add your custom data. Available in server and client. */
|
||||
custom?: Record<string, any>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FormFieldBase } from '@payloadcms/ui'
|
||||
import type { FieldMap, ReducedBlock } from '@payloadcms/ui/utilities/buildComponentMap'
|
||||
import type { FieldMap } from '@payloadcms/ui/utilities/buildComponentMap'
|
||||
import type { CollapsedPreferences, Data, FormState } from 'payload'
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js'
|
||||
@@ -20,6 +20,7 @@ import { $getNodeByKey } from 'lexical'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import type { SanitizedClientEditorConfig } from '../../../lexical/config/types.js'
|
||||
import type { ClientBlock } from '../feature.client.js'
|
||||
import type { BlockFields, BlockNode } from '../nodes/BlocksNode.js'
|
||||
|
||||
import { FormSavePlugin } from './FormSavePlugin.js'
|
||||
@@ -35,7 +36,7 @@ type Props = {
|
||||
formSchema: FieldMap
|
||||
nodeKey: string
|
||||
path: string
|
||||
reducedBlock: ReducedBlock
|
||||
reducedBlock: ClientBlock
|
||||
schemaPath: string
|
||||
}
|
||||
|
||||
@@ -51,7 +52,7 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
formData,
|
||||
formSchema,
|
||||
nodeKey,
|
||||
reducedBlock: { labels },
|
||||
reducedBlock: { LabelComponent, labels },
|
||||
schemaPath,
|
||||
} = props
|
||||
|
||||
@@ -198,34 +199,38 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
className={classNames}
|
||||
collapsibleStyle={fieldHasErrors ? 'error' : 'default'}
|
||||
header={
|
||||
<div className={`${baseClass}__block-header`}>
|
||||
<div>
|
||||
<Pill
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${formData?.blockType}`}
|
||||
pillStyle="white"
|
||||
>
|
||||
{typeof labels.singular === 'string'
|
||||
? getTranslation(labels.singular, i18n)
|
||||
: '[Singular Label]'}
|
||||
</Pill>
|
||||
<SectionTitle path="blockName" readOnly={field?.readOnly} />
|
||||
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
|
||||
LabelComponent ? (
|
||||
<LabelComponent blockKind={'lexicalBlock'} formData={formData} />
|
||||
) : (
|
||||
<div className={`${baseClass}__block-header`}>
|
||||
<div>
|
||||
<Pill
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${formData?.blockType}`}
|
||||
pillStyle="white"
|
||||
>
|
||||
{typeof labels.singular === 'string'
|
||||
? getTranslation(labels.singular, i18n)
|
||||
: '[Singular Label]'}
|
||||
</Pill>
|
||||
<SectionTitle path="blockName" readOnly={field?.readOnly} />
|
||||
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
|
||||
</div>
|
||||
{editor.isEditable() && (
|
||||
<Button
|
||||
buttonStyle="icon-label"
|
||||
className={`${baseClass}__removeButton`}
|
||||
disabled={field?.readOnly}
|
||||
icon="x"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
removeBlock()
|
||||
}}
|
||||
round
|
||||
tooltip="Remove Block"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{editor.isEditable() && (
|
||||
<Button
|
||||
buttonStyle="icon-label"
|
||||
className={`${baseClass}__removeButton`}
|
||||
disabled={field?.readOnly}
|
||||
icon="x"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
removeBlock()
|
||||
}}
|
||||
round
|
||||
tooltip="Remove Block"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
isCollapsed={isCollapsed}
|
||||
key={0}
|
||||
|
||||
@@ -18,7 +18,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { type BlockFields } from '../nodes/BlocksNode.js'
|
||||
const baseClass = 'lexical-block'
|
||||
import type { ReducedBlock } from '@payloadcms/ui/utilities/buildComponentMap'
|
||||
import type { FormState } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
@@ -26,7 +25,7 @@ import { getFormState } from '@payloadcms/ui/shared'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import type { ClientComponentProps } from '../../typesClient.js'
|
||||
import type { BlocksFeatureClientProps } from '../feature.client.js'
|
||||
import type { BlocksFeatureClientProps, ClientBlock } from '../feature.client.js'
|
||||
|
||||
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider.js'
|
||||
import { BlockContent } from './BlockContent.js'
|
||||
@@ -34,13 +33,8 @@ import './index.scss'
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode
|
||||
|
||||
formData: BlockFields
|
||||
nodeKey?: string
|
||||
/**
|
||||
* This transformedFormData already comes wrapped in blockFieldWrapperName
|
||||
*/
|
||||
transformedFormData: BlockFields
|
||||
}
|
||||
|
||||
export const BlockComponent: React.FC<Props> = (props) => {
|
||||
@@ -59,7 +53,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
const componentMapRenderedFieldsPath = `lexical_internal_feature.blocks.fields.${formData?.blockType}`
|
||||
const schemaFieldsPath = `${schemaPath}.lexical_internal_feature.blocks.${formData?.blockType}`
|
||||
|
||||
const reducedBlock: ReducedBlock = (
|
||||
const reducedBlock: ClientBlock = (
|
||||
editorConfig?.resolvedFeatureMap?.get('blocks')
|
||||
?.sanitizedClientFeatureProps as ClientComponentProps<BlocksFeatureClientProps>
|
||||
)?.reducedBlocks?.find((block) => block.slug === formData?.blockType)
|
||||
@@ -127,6 +121,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
|
||||
const classNames = [`${baseClass}__row`, `${baseClass}__row--no-errors`].filter(Boolean).join(' ')
|
||||
|
||||
const LabelComponent = reducedBlock?.LabelComponent
|
||||
|
||||
// Memoized Form JSX
|
||||
const formContent = useMemo(() => {
|
||||
return reducedBlock && initialState !== false ? (
|
||||
@@ -155,19 +151,23 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
className={classNames}
|
||||
collapsibleStyle="default"
|
||||
header={
|
||||
<div className={`${baseClass}__block-header`}>
|
||||
<div>
|
||||
<Pill
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${formData?.blockType}`}
|
||||
pillStyle="white"
|
||||
>
|
||||
{typeof reducedBlock.labels.singular === 'string'
|
||||
? getTranslation(reducedBlock.labels.singular, i18n)
|
||||
: '[Singular Label]'}
|
||||
</Pill>
|
||||
<SectionTitle path="blockName" readOnly={parentLexicalRichTextField?.readOnly} />
|
||||
LabelComponent ? (
|
||||
<LabelComponent blockKind={'lexicalBlock'} formData={formData} />
|
||||
) : (
|
||||
<div className={`${baseClass}__block-header`}>
|
||||
<div>
|
||||
<Pill
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${formData?.blockType}`}
|
||||
pillStyle="white"
|
||||
>
|
||||
{reducedBlock && typeof reducedBlock.labels.singular === 'string'
|
||||
? getTranslation(reducedBlock.labels.singular, i18n)
|
||||
: '[Singular Label]'}
|
||||
</Pill>
|
||||
<SectionTitle path="blockName" readOnly={parentLexicalRichTextField?.readOnly} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
key={0}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.inline-block-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
@extend %body;
|
||||
@include shadow-sm;
|
||||
padding: calc(var(--base) * 0.125) calc(var(--base) * 0.125) calc(var(--base) * 0.125) calc(var(--base) * 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--theme-input-bg);
|
||||
border: 1px solid var(--theme-elevation-100);
|
||||
border-radius: $style-radius-m;
|
||||
max-width: calc(var(--base) * 15);
|
||||
font-family: var(--font-body);
|
||||
margin-right: $style-stroke-width-m;
|
||||
margin-left: $style-stroke-width-m;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--theme-elevation-150);
|
||||
}
|
||||
|
||||
&__wrap {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
box-shadow: $focus-box-shadow;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&__editButton.btn {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__editButton {
|
||||
&:disabled {
|
||||
color: var(--theme-elevation-300);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: calc(var(--base) * 0.15);
|
||||
|
||||
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__removeButton.btn {
|
||||
margin: 0;
|
||||
|
||||
line {
|
||||
stroke-width: $style-stroke-width-m;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--theme-elevation-300);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
'use client'
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from 'react'
|
||||
const baseClass = 'inline-block'
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { Button, useTranslation } from '@payloadcms/ui'
|
||||
import {
|
||||
$getNodeByKey,
|
||||
$getSelection,
|
||||
$isNodeSelection,
|
||||
CLICK_COMMAND,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
KEY_BACKSPACE_COMMAND,
|
||||
KEY_DELETE_COMMAND,
|
||||
} from 'lexical'
|
||||
|
||||
import type { ClientComponentProps } from '../../typesClient.js'
|
||||
import type { BlocksFeatureClientProps, ClientBlock } from '../feature.client.js'
|
||||
import type { InlineBlockFields } from '../nodes/InlineBlocksNode.js'
|
||||
|
||||
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider.js'
|
||||
import { $isInlineBlockNode } from '../nodes/InlineBlocksNode.js'
|
||||
import { OPEN_INLINE_BLOCK_DRAWER_COMMAND } from '../plugin/commands.js'
|
||||
import './index.scss'
|
||||
|
||||
type Props = {
|
||||
readonly formData: InlineBlockFields
|
||||
readonly nodeKey?: string
|
||||
}
|
||||
|
||||
export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
const { formData, nodeKey } = props
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const { i18n, t } = useTranslation<object, string>()
|
||||
const { editorConfig, field } = useEditorConfigContext()
|
||||
const inlineBlockElemElemRef = useRef<HTMLDivElement | null>(null)
|
||||
const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey)
|
||||
|
||||
const reducedBlock: ClientBlock = (
|
||||
editorConfig?.resolvedFeatureMap?.get('blocks')
|
||||
?.sanitizedClientFeatureProps as ClientComponentProps<BlocksFeatureClientProps>
|
||||
)?.reducedInlineBlocks?.find((block) => block.slug === formData?.blockType)
|
||||
|
||||
const removeInlineBlock = useCallback(() => {
|
||||
editor.update(() => {
|
||||
$getNodeByKey(nodeKey).remove()
|
||||
})
|
||||
}, [editor, nodeKey])
|
||||
|
||||
const $onDelete = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (isSelected && $isNodeSelection($getSelection())) {
|
||||
event.preventDefault()
|
||||
const node = $getNodeByKey(nodeKey)
|
||||
if ($isInlineBlockNode(node)) {
|
||||
node.remove()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
[isSelected, nodeKey],
|
||||
)
|
||||
const onClick = useCallback(
|
||||
(payload: MouseEvent) => {
|
||||
const event = payload
|
||||
// Check if inlineBlockElemElemRef.target or anything WITHIN inlineBlockElemElemRef.target was clicked
|
||||
if (
|
||||
event.target === inlineBlockElemElemRef.current ||
|
||||
inlineBlockElemElemRef.current?.contains(event.target as Node)
|
||||
) {
|
||||
if (event.shiftKey) {
|
||||
setSelected(!isSelected)
|
||||
} else {
|
||||
if (!isSelected) {
|
||||
clearSelection()
|
||||
setSelected(true)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
[isSelected, setSelected, clearSelection],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerCommand<MouseEvent>(CLICK_COMMAND, onClick, COMMAND_PRIORITY_LOW),
|
||||
|
||||
editor.registerCommand(KEY_DELETE_COMMAND, $onDelete, COMMAND_PRIORITY_LOW),
|
||||
editor.registerCommand(KEY_BACKSPACE_COMMAND, $onDelete, COMMAND_PRIORITY_LOW),
|
||||
)
|
||||
}, [clearSelection, editor, isSelected, nodeKey, $onDelete, setSelected, onClick])
|
||||
|
||||
const LabelComponent = reducedBlock?.LabelComponent
|
||||
|
||||
const blockDisplayName = reducedBlock.labels.singular
|
||||
? getTranslation(reducedBlock.labels.singular, i18n)
|
||||
: reducedBlock.slug
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
baseClass,
|
||||
baseClass + '-' + formData.blockType,
|
||||
isSelected && `${baseClass}--selected`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
ref={inlineBlockElemElemRef}
|
||||
>
|
||||
{LabelComponent ? (
|
||||
<LabelComponent blockKind={'lexicalInlineBlock'} formData={formData} />
|
||||
) : (
|
||||
<div>{getTranslation(reducedBlock.labels.singular, i18n)}</div>
|
||||
)}
|
||||
{editor.isEditable() && (
|
||||
<div className={`${baseClass}__actions`}>
|
||||
<Button
|
||||
buttonStyle="icon-label"
|
||||
className={`${baseClass}__editButton`}
|
||||
disabled={field?.readOnly}
|
||||
el="div"
|
||||
icon="edit"
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(OPEN_INLINE_BLOCK_DRAWER_COMMAND, {
|
||||
fields: formData,
|
||||
nodeKey,
|
||||
})
|
||||
}}
|
||||
round
|
||||
size="small"
|
||||
tooltip={t('lexical:blocks:inlineBlocks:edit', { label: blockDisplayName })}
|
||||
/>
|
||||
<Button
|
||||
buttonStyle="icon-label"
|
||||
className={`${baseClass}__removeButton`}
|
||||
disabled={field?.readOnly}
|
||||
icon="x"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
removeInlineBlock()
|
||||
}}
|
||||
round
|
||||
size="small"
|
||||
tooltip={t('lexical:blocks:inlineBlocks:remove', { label: blockDisplayName })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
'use client'
|
||||
import type { LexicalCommand, LexicalEditor } from 'lexical'
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js'
|
||||
import {
|
||||
BlocksDrawer,
|
||||
formatDrawerSlug,
|
||||
useEditDepth,
|
||||
useModal,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { $getNodeByKey, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { ClientComponentProps } from '../../typesClient.js'
|
||||
import type { BlocksFeatureClientProps } from '../feature.client.js'
|
||||
|
||||
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider.js'
|
||||
import { $createBlockNode } from '../nodes/BlocksNode.js'
|
||||
import { INSERT_BLOCK_COMMAND } from '../plugin/commands.js'
|
||||
const baseClass = 'lexical-blocks-drawer'
|
||||
|
||||
export const INSERT_BLOCK_WITH_DRAWER_COMMAND: LexicalCommand<{
|
||||
replace: { nodeKey: string } | false
|
||||
}> = createCommand('INSERT_BLOCK_WITH_DRAWER_COMMAND')
|
||||
|
||||
const insertBlock = ({
|
||||
blockType,
|
||||
editor,
|
||||
replaceNodeKey,
|
||||
}: {
|
||||
blockType: string
|
||||
editor: LexicalEditor
|
||||
replaceNodeKey: null | string
|
||||
}) => {
|
||||
if (!replaceNodeKey) {
|
||||
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
||||
id: null,
|
||||
blockName: '',
|
||||
blockType,
|
||||
})
|
||||
} else {
|
||||
editor.update(() => {
|
||||
const node = $getNodeByKey(replaceNodeKey)
|
||||
if (node) {
|
||||
node.replace(
|
||||
$createBlockNode({
|
||||
id: null,
|
||||
blockName: '',
|
||||
blockType,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const BlocksDrawerComponent: React.FC = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const { editorConfig, uuid } = useEditorConfigContext()
|
||||
|
||||
const [replaceNodeKey, setReplaceNodeKey] = useState<null | string>(null)
|
||||
const editDepth = useEditDepth()
|
||||
const { t } = useTranslation()
|
||||
const { openModal } = useModal()
|
||||
|
||||
const labels = {
|
||||
plural: t('fields:blocks') || 'Blocks',
|
||||
singular: t('fields:block') || 'Block',
|
||||
}
|
||||
|
||||
const addRow = useCallback(
|
||||
(rowIndex: number, blockType: string) => {
|
||||
insertBlock({
|
||||
blockType,
|
||||
editor,
|
||||
replaceNodeKey,
|
||||
})
|
||||
},
|
||||
[editor, replaceNodeKey],
|
||||
)
|
||||
|
||||
const drawerSlug = formatDrawerSlug({
|
||||
slug: `lexical-rich-text-blocks-` + uuid,
|
||||
depth: editDepth,
|
||||
})
|
||||
|
||||
const reducedBlocks = (
|
||||
editorConfig?.resolvedFeatureMap?.get('blocks')
|
||||
?.sanitizedClientFeatureProps as ClientComponentProps<BlocksFeatureClientProps>
|
||||
)?.reducedBlocks
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerCommand<{
|
||||
replace: { nodeKey: string } | false
|
||||
}>(
|
||||
INSERT_BLOCK_WITH_DRAWER_COMMAND,
|
||||
(payload) => {
|
||||
setReplaceNodeKey(payload?.replace ? payload?.replace.nodeKey : null)
|
||||
openModal(drawerSlug)
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
)
|
||||
}, [editor, drawerSlug, openModal])
|
||||
|
||||
return (
|
||||
<BlocksDrawer
|
||||
addRow={addRow}
|
||||
addRowIndex={0}
|
||||
blocks={reducedBlocks}
|
||||
drawerSlug={drawerSlug}
|
||||
labels={labels}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import type { ReducedBlock } from '@payloadcms/ui/utilities/buildComponentMap'
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
import type { ToolbarGroup } from '../toolbars/types.js'
|
||||
|
||||
import { BlockIcon } from '../../lexical/ui/icons/Block/index.js'
|
||||
import { InlineBlocksIcon } from '../../lexical/ui/icons/InlineBlocks/index.js'
|
||||
import { createClientFeature } from '../../utilities/createClientFeature.js'
|
||||
import { BlockNode } from './nodes/BlocksNode.js'
|
||||
import { INSERT_BLOCK_COMMAND } from './plugin/commands.js'
|
||||
import { InlineBlockNode } from './nodes/InlineBlocksNode.js'
|
||||
import { INSERT_BLOCK_COMMAND, OPEN_INLINE_BLOCK_DRAWER_COMMAND } from './plugin/commands.js'
|
||||
import { BlocksPlugin } from './plugin/index.js'
|
||||
|
||||
export type ClientBlock = {
|
||||
LabelComponent?: Block['admin']['components']['Label']
|
||||
} & ReducedBlock
|
||||
|
||||
export type BlocksFeatureClientProps = {
|
||||
reducedBlocks: ReducedBlock[]
|
||||
reducedBlocks: ClientBlock[]
|
||||
reducedInlineBlocks: ClientBlock[]
|
||||
}
|
||||
|
||||
export const BlocksFeatureClient = createClientFeature<BlocksFeatureClientProps>(({ props }) => ({
|
||||
nodes: [BlockNode],
|
||||
nodes: [BlockNode, InlineBlockNode],
|
||||
plugins: [
|
||||
{
|
||||
Component: BlocksPlugin,
|
||||
@@ -25,65 +35,130 @@ export const BlocksFeatureClient = createClientFeature<BlocksFeatureClientProps>
|
||||
sanitizedClientFeatureProps: props,
|
||||
slashMenu: {
|
||||
groups: [
|
||||
{
|
||||
items: props.reducedBlocks.map((block) => {
|
||||
return {
|
||||
Icon: BlockIcon,
|
||||
key: 'block-' + block.slug,
|
||||
keywords: ['block', 'blocks', block.slug],
|
||||
label: ({ i18n }) => {
|
||||
if (!block.labels.singular) {
|
||||
return block.slug
|
||||
}
|
||||
props.reducedBlocks?.length
|
||||
? {
|
||||
items: props.reducedBlocks.map((block) => {
|
||||
return {
|
||||
Icon: BlockIcon,
|
||||
key: 'block-' + block.slug,
|
||||
keywords: ['block', 'blocks', block.slug],
|
||||
label: ({ i18n }) => {
|
||||
if (!block.labels.singular) {
|
||||
return block.slug
|
||||
}
|
||||
|
||||
return getTranslation(block.labels.singular, i18n)
|
||||
},
|
||||
onSelect: ({ editor }) => {
|
||||
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
||||
id: null,
|
||||
blockName: '',
|
||||
blockType: block.slug,
|
||||
})
|
||||
return getTranslation(block.labels.singular, i18n)
|
||||
},
|
||||
onSelect: ({ editor }) => {
|
||||
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
||||
id: null,
|
||||
blockName: '',
|
||||
blockType: block.slug,
|
||||
})
|
||||
},
|
||||
}
|
||||
}),
|
||||
key: 'blocks',
|
||||
label: ({ i18n }) => {
|
||||
return i18n.t('lexical:blocks:label')
|
||||
},
|
||||
}
|
||||
}),
|
||||
key: 'blocks',
|
||||
label: ({ i18n }) => {
|
||||
return i18n.t('lexical:blocks:label')
|
||||
},
|
||||
},
|
||||
],
|
||||
: null,
|
||||
props.reducedInlineBlocks?.length
|
||||
? {
|
||||
items: props.reducedInlineBlocks.map((inlineBlock) => {
|
||||
return {
|
||||
Icon: InlineBlocksIcon,
|
||||
key: 'inlineBlocks-' + inlineBlock.slug,
|
||||
keywords: ['inlineBlock', 'inline block', inlineBlock.slug],
|
||||
label: ({ i18n }) => {
|
||||
if (!inlineBlock.labels.singular) {
|
||||
return inlineBlock.slug
|
||||
}
|
||||
|
||||
return getTranslation(inlineBlock.labels.singular, i18n)
|
||||
},
|
||||
onSelect: ({ editor }) => {
|
||||
editor.dispatchCommand(OPEN_INLINE_BLOCK_DRAWER_COMMAND, {
|
||||
fields: {
|
||||
id: null,
|
||||
blockName: '',
|
||||
blockType: inlineBlock.slug,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
}),
|
||||
key: 'inlineBlocks',
|
||||
label: ({ i18n }) => {
|
||||
return i18n.t('lexical:blocks:inlineBlocks:label')
|
||||
},
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean),
|
||||
},
|
||||
toolbarFixed: {
|
||||
groups: [
|
||||
{
|
||||
type: 'dropdown',
|
||||
ChildComponent: BlockIcon,
|
||||
items: props.reducedBlocks.map((block, index) => {
|
||||
return {
|
||||
props.reducedBlocks?.length
|
||||
? {
|
||||
type: 'dropdown',
|
||||
ChildComponent: BlockIcon,
|
||||
isActive: undefined, // At this point, we would be inside a sub-richtext-editor. And at this point this will be run against the focused sub-editor, not the parent editor which has the actual block. Thus, no point in running this
|
||||
key: 'block-' + block.slug,
|
||||
label: ({ i18n }) => {
|
||||
if (!block.labels.singular) {
|
||||
return block.slug
|
||||
}
|
||||
items: props.reducedBlocks.map((block, index) => {
|
||||
return {
|
||||
ChildComponent: BlockIcon,
|
||||
isActive: undefined, // At this point, we would be inside a sub-richtext-editor. And at this point this will be run against the focused sub-editor, not the parent editor which has the actual block. Thus, no point in running this
|
||||
key: 'block-' + block.slug,
|
||||
label: ({ i18n }) => {
|
||||
if (!block.labels.singular) {
|
||||
return block.slug
|
||||
}
|
||||
|
||||
return getTranslation(block.labels.singular, i18n)
|
||||
},
|
||||
onSelect: ({ editor }) => {
|
||||
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
||||
id: null,
|
||||
blockName: '',
|
||||
blockType: block.slug,
|
||||
})
|
||||
},
|
||||
order: index,
|
||||
return getTranslation(block.labels.singular, i18n)
|
||||
},
|
||||
onSelect: ({ editor }) => {
|
||||
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
||||
id: null,
|
||||
blockName: '',
|
||||
blockType: block.slug,
|
||||
})
|
||||
},
|
||||
order: index,
|
||||
}
|
||||
}),
|
||||
key: 'blocks',
|
||||
order: 20,
|
||||
}
|
||||
}),
|
||||
key: 'blocks',
|
||||
order: 20,
|
||||
},
|
||||
],
|
||||
: null,
|
||||
props.reducedInlineBlocks?.length
|
||||
? {
|
||||
type: 'dropdown',
|
||||
ChildComponent: InlineBlocksIcon,
|
||||
items: props.reducedInlineBlocks.map((inlineBlock, index) => {
|
||||
return {
|
||||
ChildComponent: InlineBlocksIcon,
|
||||
isActive: undefined,
|
||||
key: 'inlineBlock-' + inlineBlock.slug,
|
||||
label: ({ i18n }) => {
|
||||
if (!inlineBlock.labels.singular) {
|
||||
return inlineBlock.slug
|
||||
}
|
||||
|
||||
return getTranslation(inlineBlock.labels.singular, i18n)
|
||||
},
|
||||
onSelect: ({ editor }) => {
|
||||
editor.dispatchCommand(OPEN_INLINE_BLOCK_DRAWER_COMMAND, {
|
||||
fields: {
|
||||
blockType: inlineBlock.slug,
|
||||
},
|
||||
})
|
||||
},
|
||||
order: index,
|
||||
}
|
||||
}),
|
||||
key: 'inlineBlocks',
|
||||
order: 25,
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean) as ToolbarGroup[],
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -11,10 +11,12 @@ import { createNode } from '../typeUtilities.js'
|
||||
import { blockPopulationPromiseHOC } from './graphQLPopulationPromise.js'
|
||||
import { i18n } from './i18n.js'
|
||||
import { BlockNode } from './nodes/BlocksNode.js'
|
||||
import { InlineBlockNode } from './nodes/InlineBlocksNode.js'
|
||||
import { blockValidationHOC } from './validate.js'
|
||||
|
||||
export type BlocksFeatureProps = {
|
||||
blocks: Block[]
|
||||
blocks?: Block[]
|
||||
inlineBlocks?: Block[]
|
||||
}
|
||||
|
||||
export const BlocksFeature = createServerFeature<
|
||||
@@ -23,34 +25,65 @@ export const BlocksFeature = createServerFeature<
|
||||
BlocksFeatureClientProps
|
||||
>({
|
||||
feature: async ({ config: _config, isRoot, props }) => {
|
||||
if (props?.blocks?.length) {
|
||||
if (props?.blocks?.length || props?.inlineBlocks?.length) {
|
||||
const validRelationships = _config.collections.map((c) => c.slug) || []
|
||||
|
||||
for (const block of props.blocks) {
|
||||
block.fields = block.fields.concat(baseBlockFields)
|
||||
block.labels = !block.labels ? formatLabels(block.slug) : block.labels
|
||||
if (props?.blocks?.length) {
|
||||
for (const block of props.blocks) {
|
||||
block.fields = block.fields.concat(baseBlockFields)
|
||||
block.labels = !block.labels ? formatLabels(block.slug) : block.labels
|
||||
|
||||
block.fields = await sanitizeFields({
|
||||
config: _config as unknown as Config,
|
||||
fields: block.fields,
|
||||
requireFieldLevelRichTextEditor: isRoot,
|
||||
validRelationships,
|
||||
})
|
||||
block.fields = await sanitizeFields({
|
||||
config: _config as unknown as Config,
|
||||
fields: block.fields,
|
||||
requireFieldLevelRichTextEditor: isRoot,
|
||||
validRelationships,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (props?.inlineBlocks?.length) {
|
||||
for (const block of props.inlineBlocks) {
|
||||
block.fields = block.fields.concat(baseBlockFields)
|
||||
block.labels = !block.labels ? formatLabels(block.slug) : block.labels
|
||||
|
||||
block.fields = await sanitizeFields({
|
||||
config: _config as unknown as Config,
|
||||
fields: block.fields,
|
||||
requireFieldLevelRichTextEditor: isRoot,
|
||||
validRelationships,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build clientProps
|
||||
const clientProps: BlocksFeatureClientProps = {
|
||||
reducedBlocks: [],
|
||||
reducedInlineBlocks: [],
|
||||
}
|
||||
for (const block of props.blocks) {
|
||||
clientProps.reducedBlocks.push({
|
||||
slug: block.slug,
|
||||
fieldMap: [],
|
||||
imageAltText: block.imageAltText,
|
||||
imageURL: block.imageURL,
|
||||
labels: block.labels,
|
||||
})
|
||||
if (props?.blocks?.length) {
|
||||
for (const block of props.blocks) {
|
||||
clientProps.reducedBlocks.push({
|
||||
slug: block.slug,
|
||||
LabelComponent: block?.admin?.components?.Label,
|
||||
fieldMap: [],
|
||||
imageAltText: block.imageAltText,
|
||||
imageURL: block.imageURL,
|
||||
labels: block.labels,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (props?.inlineBlocks?.length) {
|
||||
for (const block of props.inlineBlocks) {
|
||||
clientProps.reducedInlineBlocks.push({
|
||||
slug: block.slug,
|
||||
LabelComponent: block?.admin?.components?.Label,
|
||||
fieldMap: [],
|
||||
imageAltText: block.imageAltText,
|
||||
imageURL: block.imageURL,
|
||||
labels: block.labels,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -63,8 +96,16 @@ export const BlocksFeature = createServerFeature<
|
||||
*/
|
||||
const schemaMap = new Map<string, Field[]>()
|
||||
|
||||
for (const block of props.blocks) {
|
||||
schemaMap.set(block.slug, block.fields || [])
|
||||
if (props?.blocks?.length) {
|
||||
for (const block of props.blocks) {
|
||||
schemaMap.set(block.slug, block.fields || [])
|
||||
}
|
||||
}
|
||||
|
||||
if (props?.inlineBlocks?.length) {
|
||||
for (const block of props.inlineBlocks) {
|
||||
schemaMap.set(block.slug, block.fields || [])
|
||||
}
|
||||
}
|
||||
|
||||
return schemaMap
|
||||
@@ -77,23 +118,32 @@ export const BlocksFeature = createServerFeature<
|
||||
field,
|
||||
interfaceNameDefinitions,
|
||||
}) => {
|
||||
if (!props?.blocks?.length) {
|
||||
if (!props?.blocks?.length && !props?.inlineBlocks?.length) {
|
||||
return currentSchema
|
||||
}
|
||||
|
||||
const blocksField: BlockField = {
|
||||
name: field?.name + '_lexical_blocks',
|
||||
type: 'blocks',
|
||||
blocks: props.blocks,
|
||||
const fields: BlockField[] = []
|
||||
|
||||
if (props?.blocks?.length) {
|
||||
fields.push({
|
||||
name: field?.name + '_lexical_blocks',
|
||||
type: 'blocks',
|
||||
blocks: props.blocks,
|
||||
})
|
||||
}
|
||||
if (props?.inlineBlocks?.length) {
|
||||
fields.push({
|
||||
name: field?.name + '_lexical_inline_blocks',
|
||||
type: 'blocks',
|
||||
blocks: props.inlineBlocks,
|
||||
})
|
||||
}
|
||||
|
||||
if (fields.length) {
|
||||
// This is only done so that interfaceNameDefinitions sets those block's interfaceNames.
|
||||
// we don't actually use the JSON Schema itself in the generated types yet.
|
||||
fieldsToJSONSchema(collectionIDFieldTypes, fields, interfaceNameDefinitions, config)
|
||||
}
|
||||
// This is only done so that interfaceNameDefinitions sets those block's interfaceNames.
|
||||
// we don't actually use the JSON Schema itself in the generated types yet.
|
||||
fieldsToJSONSchema(
|
||||
collectionIDFieldTypes,
|
||||
[blocksField],
|
||||
interfaceNameDefinitions,
|
||||
config,
|
||||
)
|
||||
|
||||
return currentSchema
|
||||
},
|
||||
@@ -110,9 +160,23 @@ export const BlocksFeature = createServerFeature<
|
||||
getSubFieldsData: ({ node }) => {
|
||||
return node?.fields
|
||||
},
|
||||
graphQLPopulationPromises: [blockPopulationPromiseHOC(props)],
|
||||
graphQLPopulationPromises: [blockPopulationPromiseHOC(props.blocks)],
|
||||
node: BlockNode,
|
||||
validations: [blockValidationHOC(props)],
|
||||
validations: [blockValidationHOC(props.blocks)],
|
||||
}),
|
||||
createNode({
|
||||
getSubFields: ({ node }) => {
|
||||
const blockType = node.fields.blockType
|
||||
|
||||
const block = props.inlineBlocks.find((block) => block.slug === blockType)
|
||||
return block?.fields
|
||||
},
|
||||
getSubFieldsData: ({ node }) => {
|
||||
return node?.fields
|
||||
},
|
||||
graphQLPopulationPromises: [blockPopulationPromiseHOC(props.inlineBlocks)],
|
||||
node: InlineBlockNode,
|
||||
validations: [blockValidationHOC(props.inlineBlocks)],
|
||||
}),
|
||||
],
|
||||
sanitizedServerFeatureProps: props,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import type { PopulationPromise } from '../typesServer.js'
|
||||
import type { BlocksFeatureProps } from './feature.server.js'
|
||||
import type { SerializedBlockNode } from './nodes/BlocksNode.js'
|
||||
import type { SerializedInlineBlockNode } from './nodes/InlineBlocksNode.js'
|
||||
|
||||
import { recursivelyPopulateFieldsForGraphQL } from '../../populateGraphQL/recursivelyPopulateFieldsForGraphQL.js'
|
||||
|
||||
export const blockPopulationPromiseHOC = (
|
||||
props: BlocksFeatureProps,
|
||||
): PopulationPromise<SerializedBlockNode> => {
|
||||
blocks: Block[],
|
||||
): PopulationPromise<SerializedBlockNode | SerializedInlineBlockNode> => {
|
||||
const blockPopulationPromise: PopulationPromise<SerializedBlockNode> = ({
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -25,7 +27,7 @@ export const blockPopulationPromiseHOC = (
|
||||
const blockFieldData = node.fields
|
||||
|
||||
// find block used in this node
|
||||
const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
|
||||
const block = blocks.find((block) => block.slug === blockFieldData.blockType)
|
||||
if (!block || !block?.fields?.length || !blockFieldData) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,99 +2,291 @@ import type { GenericLanguages } from '@payloadcms/translations'
|
||||
|
||||
export const i18n: Partial<GenericLanguages> = {
|
||||
ar: {
|
||||
inlineBlocks: {
|
||||
create: 'أنشئ {{label}}',
|
||||
edit: 'تحرير {{التسمية}}',
|
||||
label: 'الكتل الداخلية',
|
||||
remove: 'إزالة {{التسمية}}',
|
||||
},
|
||||
label: 'كتل',
|
||||
},
|
||||
az: {
|
||||
inlineBlocks: {
|
||||
create: 'Yarat {{label}}',
|
||||
edit: '{{label}} redaktə et',
|
||||
label: 'Sıralı Bloklar',
|
||||
remove: '{{label}} silin',
|
||||
},
|
||||
label: 'Bloklar',
|
||||
},
|
||||
bg: {
|
||||
inlineBlocks: {
|
||||
create: 'Създайте {{етикет}}',
|
||||
edit: 'Редактирай {{етикет}}',
|
||||
label: 'Вградени блокове',
|
||||
remove: 'Премахнете {{етикет}}',
|
||||
},
|
||||
label: 'Блокове',
|
||||
},
|
||||
cs: {
|
||||
inlineBlocks: {
|
||||
create: 'Vytvořte {{štítek}}',
|
||||
edit: 'Upravit {{label}}',
|
||||
label: 'Inline bloky',
|
||||
remove: 'Odstraňte {{label}}',
|
||||
},
|
||||
label: 'Bloky',
|
||||
},
|
||||
de: {
|
||||
inlineBlocks: {
|
||||
create: 'Erstelle {{label}}',
|
||||
edit: 'Bearbeite {{label}}',
|
||||
label: 'Inline-Blöcke',
|
||||
remove: 'Entferne {{label}}',
|
||||
},
|
||||
label: 'Blöcke',
|
||||
},
|
||||
en: {
|
||||
inlineBlocks: {
|
||||
create: 'Create {{label}}',
|
||||
edit: 'Edit {{label}}',
|
||||
label: 'Inline Blocks',
|
||||
remove: 'Remove {{label}}',
|
||||
},
|
||||
label: 'Blocks',
|
||||
},
|
||||
es: {
|
||||
inlineBlocks: {
|
||||
create: 'Crear {{etiqueta}}',
|
||||
edit: 'Editar {{etiqueta}}',
|
||||
label: 'Bloques en línea',
|
||||
remove: 'Eliminar {{label}}',
|
||||
},
|
||||
label: 'Bloques',
|
||||
},
|
||||
fa: {
|
||||
inlineBlocks: {
|
||||
create: 'ایجاد {{برچسب}}',
|
||||
edit: 'ویرایش {{برچسب}}',
|
||||
label: 'بلوکهای درون خطی',
|
||||
remove: 'حذف {{برچسب}}',
|
||||
},
|
||||
label: 'بلوک ها',
|
||||
},
|
||||
fr: {
|
||||
inlineBlocks: {
|
||||
create: 'Créer {{label}}',
|
||||
edit: 'Modifier {{label}}',
|
||||
label: 'Blocs en ligne',
|
||||
remove: 'Supprimer {{label}}',
|
||||
},
|
||||
label: 'Blocs',
|
||||
},
|
||||
he: {
|
||||
inlineBlocks: {
|
||||
create: 'צור {{תווית}}',
|
||||
edit: 'ערוך {{תווית}}',
|
||||
label: 'בלוקים משורשרים',
|
||||
remove: 'הסר {{תווית}}',
|
||||
},
|
||||
label: 'חסימות',
|
||||
},
|
||||
hr: {
|
||||
inlineBlocks: {
|
||||
create: 'Stvori {{oznaka}}',
|
||||
edit: 'Uredi {{label}}',
|
||||
label: 'Unutrašnji blokovi',
|
||||
remove: 'Ukloni {{oznaka}}',
|
||||
},
|
||||
label: 'Blokovi',
|
||||
},
|
||||
hu: {
|
||||
inlineBlocks: {
|
||||
create: 'Hozzon létre {{címke}}',
|
||||
edit: 'Szerkesztés {{címke}}',
|
||||
label: 'Beágyazott blokkok',
|
||||
remove: 'Távolítsa el a {{label}}',
|
||||
},
|
||||
label: 'Blokkok',
|
||||
},
|
||||
it: {
|
||||
inlineBlocks: {
|
||||
create: 'Crea {{etichetta}}',
|
||||
edit: 'Modifica {{label}}',
|
||||
label: 'Blocchi in linea',
|
||||
remove: 'Rimuovi {{label}}',
|
||||
},
|
||||
label: 'Blocchi',
|
||||
},
|
||||
ja: {
|
||||
inlineBlocks: {
|
||||
create: '{{label}}を作成する',
|
||||
edit: '{{label}}を編集する',
|
||||
label: 'インラインブロック',
|
||||
remove: '{{ラベル}}を削除します',
|
||||
},
|
||||
label: 'ブロック',
|
||||
},
|
||||
ko: {
|
||||
inlineBlocks: {
|
||||
create: '{{label}} 생성하기',
|
||||
edit: '{{label}} 수정하기',
|
||||
label: '인라인 블록',
|
||||
remove: '{{label}} 제거하세요',
|
||||
},
|
||||
label: '블록',
|
||||
},
|
||||
my: {
|
||||
inlineBlocks: {
|
||||
create: 'Cipta {{label}}',
|
||||
edit: 'Sunting {{label}}',
|
||||
label: 'Inline Blocks [SKIPPED]',
|
||||
remove: 'Buang {{label}}',
|
||||
},
|
||||
label: 'တံတားများ',
|
||||
},
|
||||
nb: {
|
||||
inlineBlocks: {
|
||||
create: 'Opprett {{label}}',
|
||||
edit: 'Rediger {{label}}',
|
||||
label: 'In-line blokker',
|
||||
remove: 'Fjern {{label}}',
|
||||
},
|
||||
label: 'Blokker',
|
||||
},
|
||||
nl: {
|
||||
inlineBlocks: {
|
||||
create: 'Maak {{label}}',
|
||||
edit: 'Bewerk {{label}}',
|
||||
label: 'Inline Blocks',
|
||||
remove: 'Verwijder {{label}}',
|
||||
},
|
||||
label: 'Blokken',
|
||||
},
|
||||
pl: {
|
||||
inlineBlocks: {
|
||||
create: 'Utwórz {{label}}',
|
||||
edit: 'Edytuj {{etykieta}}',
|
||||
label: 'Blokowanie w linii',
|
||||
remove: 'Usuń {{etykieta}}',
|
||||
},
|
||||
label: 'Bloki',
|
||||
},
|
||||
pt: {
|
||||
inlineBlocks: {
|
||||
create: 'Crie {{label}}',
|
||||
edit: 'Editar {{label}}',
|
||||
label: 'Blocos em linha',
|
||||
remove: 'Remova {{label}}',
|
||||
},
|
||||
label: 'Blocos',
|
||||
},
|
||||
ro: {
|
||||
inlineBlocks: {
|
||||
create: 'Creează {{eticheta}}',
|
||||
edit: 'Editați {{eticheta}}',
|
||||
label: 'Blocuri in linie',
|
||||
remove: 'Ștergeți {{etichetă}}',
|
||||
},
|
||||
label: 'Blocuri',
|
||||
},
|
||||
rs: {
|
||||
inlineBlocks: {
|
||||
create: 'Kreiraj {{label}}',
|
||||
edit: 'Izmeni {{label}}',
|
||||
label: 'Umetnuti blokovi',
|
||||
remove: 'Ukloni {{label}}',
|
||||
},
|
||||
label: 'Blokovi',
|
||||
},
|
||||
'rs-latin': {
|
||||
inlineBlocks: {
|
||||
create: 'Kreiraj {{label}}',
|
||||
edit: 'Izmeni {{label}}',
|
||||
label: 'Unutar blokovi',
|
||||
remove: 'Ukloni {{oznaka}}',
|
||||
},
|
||||
label: 'Blokovi',
|
||||
},
|
||||
ru: {
|
||||
inlineBlocks: {
|
||||
create: 'Создать {{label}}',
|
||||
edit: 'Изменить {{метка}}',
|
||||
label: 'Встроенные блоки',
|
||||
remove: 'Удалить {{метка}}',
|
||||
},
|
||||
label: 'Блоки',
|
||||
},
|
||||
sk: {
|
||||
inlineBlocks: {
|
||||
create: 'Vytvorte {{označenie}}',
|
||||
edit: 'Upraviť {{label}}',
|
||||
label: 'Inline bloky',
|
||||
remove: 'Odstráňte {{label}}',
|
||||
},
|
||||
label: 'Bloky',
|
||||
},
|
||||
sv: {
|
||||
inlineBlocks: {
|
||||
create: 'Skapa {{label}}',
|
||||
edit: 'Redigera {{etikett}}',
|
||||
label: 'Inline-blockar',
|
||||
remove: 'Ta bort {{etikett}}',
|
||||
},
|
||||
label: 'Block',
|
||||
},
|
||||
th: {
|
||||
inlineBlocks: {
|
||||
create: 'สร้าง {{label}}',
|
||||
edit: 'แก้ไข {{label}}',
|
||||
label: 'บล็อกแบบอินไลน์',
|
||||
remove: 'ลบ {{label}}',
|
||||
},
|
||||
label: 'บล็อค',
|
||||
},
|
||||
tr: {
|
||||
inlineBlocks: {
|
||||
create: '{{Etiketi}} oluşturun',
|
||||
edit: "{{Etiket}}'i düzenleyin",
|
||||
label: 'Satır İçi Bloklar',
|
||||
remove: '{{Etiketi}} kaldırın',
|
||||
},
|
||||
label: 'Bloklar',
|
||||
},
|
||||
uk: {
|
||||
inlineBlocks: {
|
||||
create: 'Створити {{label}}',
|
||||
edit: 'Редагувати {{label}}',
|
||||
label: 'Вбудовані блоки',
|
||||
remove: 'Видалити {{мітку}}',
|
||||
},
|
||||
label: 'Блоки',
|
||||
},
|
||||
vi: {
|
||||
inlineBlocks: {
|
||||
create: 'Tạo {{label}}',
|
||||
edit: 'Chỉnh sửa {{nhãn}}',
|
||||
label: 'Khối nội tuyến',
|
||||
remove: 'Xóa {{nhãn}}',
|
||||
},
|
||||
label: 'Khối',
|
||||
},
|
||||
zh: {
|
||||
inlineBlocks: {
|
||||
create: '创建{{label}}',
|
||||
edit: '编辑 {{label}}',
|
||||
label: '内联块',
|
||||
remove: '删除{{label}}',
|
||||
},
|
||||
label: '块',
|
||||
},
|
||||
'zh-TW': {
|
||||
inlineBlocks: {
|
||||
create: '創建 {{label}}',
|
||||
edit: '編輯 {{label}}',
|
||||
label: '內聯區塊',
|
||||
remove: '移除 {{label}}',
|
||||
},
|
||||
label: '區塊',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -88,7 +88,6 @@ export class BlockNode extends DecoratorBlockNode {
|
||||
return false
|
||||
}
|
||||
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
|
||||
// @ts-expect-error
|
||||
return <BlockComponent formData={this.getFields()} nodeKey={this.getKey()} />
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import type {
|
||||
DOMConversionMap,
|
||||
DOMExportOutput,
|
||||
EditorConfig,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
Spread,
|
||||
} from 'lexical'
|
||||
|
||||
import ObjectID from 'bson-objectid'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import React, { type JSX } from 'react'
|
||||
|
||||
export type InlineBlockFields = {
|
||||
/** Block form data */
|
||||
[key: string]: any
|
||||
//blockName: string
|
||||
blockType: string
|
||||
id: string
|
||||
}
|
||||
|
||||
const InlineBlockComponent = React.lazy(() =>
|
||||
import('../componentInline/index.js').then((module) => ({
|
||||
default: module.InlineBlockComponent,
|
||||
})),
|
||||
)
|
||||
|
||||
export type SerializedInlineBlockNode = Spread<
|
||||
{
|
||||
children?: never // required so that our typed editor state doesn't automatically add children
|
||||
fields: InlineBlockFields
|
||||
type: 'inlineBlock'
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>
|
||||
|
||||
export class InlineBlockNode extends DecoratorNode<React.ReactElement> {
|
||||
__fields: InlineBlockFields
|
||||
|
||||
constructor({ fields, key }: { fields: InlineBlockFields; key?: NodeKey }) {
|
||||
super(key)
|
||||
this.__fields = fields
|
||||
}
|
||||
|
||||
static clone(node: InlineBlockNode): InlineBlockNode {
|
||||
return new InlineBlockNode({
|
||||
fields: node.__fields,
|
||||
key: node.__key,
|
||||
})
|
||||
}
|
||||
|
||||
static getType(): string {
|
||||
return 'inlineBlock'
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap<HTMLDivElement> | null {
|
||||
return {}
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedInlineBlockNode): InlineBlockNode {
|
||||
const node = $createInlineBlockNode(serializedNode.fields)
|
||||
return node
|
||||
}
|
||||
|
||||
static isInline(): false {
|
||||
return false
|
||||
}
|
||||
canIndent() {
|
||||
return true
|
||||
}
|
||||
|
||||
createDOM() {
|
||||
const element = document.createElement('span')
|
||||
element.classList.add('inline-block-container')
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
|
||||
return <InlineBlockComponent formData={this.getFields()} nodeKey={this.getKey()} />
|
||||
}
|
||||
|
||||
exportDOM(): DOMExportOutput {
|
||||
const element = document.createElement('span')
|
||||
element.classList.add('inline-block-container')
|
||||
|
||||
const text = document.createTextNode(this.getTextContent())
|
||||
element.append(text)
|
||||
return { element }
|
||||
}
|
||||
|
||||
exportJSON(): SerializedInlineBlockNode {
|
||||
return {
|
||||
type: 'inlineBlock',
|
||||
fields: this.getFields(),
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
getFields(): InlineBlockFields {
|
||||
return this.getLatest().__fields
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return `Block Field`
|
||||
}
|
||||
|
||||
isInline() {
|
||||
return true
|
||||
}
|
||||
|
||||
setFields(fields: InlineBlockFields): void {
|
||||
const fieldsCopy = JSON.parse(JSON.stringify(fields)) as InlineBlockFields
|
||||
|
||||
const writable = this.getWritable()
|
||||
writable.__fields = fieldsCopy
|
||||
}
|
||||
|
||||
updateDOM(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function $createInlineBlockNode(fields: Exclude<InlineBlockFields, 'id'>): InlineBlockNode {
|
||||
return new InlineBlockNode({
|
||||
fields: {
|
||||
...fields,
|
||||
id: fields?.id || new ObjectID.default().toHexString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function $isInlineBlockNode(
|
||||
node: InlineBlockNode | LexicalNode | null | undefined,
|
||||
): node is InlineBlockNode {
|
||||
return node instanceof InlineBlockNode
|
||||
}
|
||||
@@ -6,3 +6,11 @@ import type { InsertBlockPayload } from './index.js'
|
||||
|
||||
export const INSERT_BLOCK_COMMAND: LexicalCommand<InsertBlockPayload> =
|
||||
createCommand('INSERT_BLOCK_COMMAND')
|
||||
|
||||
export const INSERT_INLINE_BLOCK_COMMAND: LexicalCommand<Partial<InsertBlockPayload>> =
|
||||
createCommand('INSERT_INLINE_BLOCK_COMMAND')
|
||||
|
||||
export const OPEN_INLINE_BLOCK_DRAWER_COMMAND: LexicalCommand<{
|
||||
fields: Partial<InsertBlockPayload>
|
||||
nodeKey?: string
|
||||
}> = createCommand('OPEN_INLINE_BLOCK_DRAWER_COMMAND')
|
||||
|
||||
@@ -1,27 +1,55 @@
|
||||
'use client'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js'
|
||||
import { $insertNodeToNearestRoot, mergeRegister } from '@lexical/utils'
|
||||
import { $insertNodeToNearestRoot, $wrapNodeInElement, mergeRegister } from '@lexical/utils'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useModal, useTranslation } from '@payloadcms/ui'
|
||||
import {
|
||||
$createParagraphNode,
|
||||
$getNodeByKey,
|
||||
$getPreviousSelection,
|
||||
$getSelection,
|
||||
$insertNodes,
|
||||
$isParagraphNode,
|
||||
$isRangeSelection,
|
||||
$isRootOrShadowRoot,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
type RangeSelection,
|
||||
} from 'lexical'
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import type { PluginComponent } from '../../typesClient.js'
|
||||
import type { BlocksFeatureClientProps } from '../feature.client.js'
|
||||
import type { ClientComponentProps, PluginComponent } from '../../typesClient.js'
|
||||
import type { BlocksFeatureClientProps, ClientBlock } from '../feature.client.js'
|
||||
import type { BlockFields } from '../nodes/BlocksNode.js'
|
||||
import type { InlineBlockNode } from '../nodes/InlineBlocksNode.js'
|
||||
|
||||
import { BlocksDrawerComponent } from '../drawer/index.js'
|
||||
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider.js'
|
||||
import { FieldsDrawer } from '../../../utilities/fieldsDrawer/Drawer.js'
|
||||
import { $createBlockNode, BlockNode } from '../nodes/BlocksNode.js'
|
||||
import { INSERT_BLOCK_COMMAND } from './commands.js'
|
||||
import { $createInlineBlockNode } from '../nodes/InlineBlocksNode.js'
|
||||
import {
|
||||
INSERT_BLOCK_COMMAND,
|
||||
INSERT_INLINE_BLOCK_COMMAND,
|
||||
OPEN_INLINE_BLOCK_DRAWER_COMMAND,
|
||||
} from './commands.js'
|
||||
|
||||
export type InsertBlockPayload = Exclude<BlockFields, 'id'>
|
||||
|
||||
const drawerSlug = 'lexical-inlineBlocks-create'
|
||||
|
||||
export const BlocksPlugin: PluginComponent<BlocksFeatureClientProps> = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const { closeModal, toggleModal } = useModal()
|
||||
const [blockFields, setBlockFields] = useState<BlockFields>(null)
|
||||
const [blockType, setBlockType] = useState<string>('' as any)
|
||||
const [targetNodeKey, setTargetNodeKey] = useState<null | string>(null)
|
||||
const { i18n, t } = useTranslation<string, any>()
|
||||
|
||||
const { editorConfig } = useEditorConfigContext()
|
||||
|
||||
const reducedBlock: ClientBlock = (
|
||||
editorConfig?.resolvedFeatureMap?.get('blocks')
|
||||
?.sanitizedClientFeatureProps as ClientComponentProps<BlocksFeatureClientProps>
|
||||
)?.reducedInlineBlocks?.find((block) => block.slug === blockFields?.blockType)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([BlockNode])) {
|
||||
@@ -62,8 +90,86 @@ export const BlocksPlugin: PluginComponent<BlocksFeatureClientProps> = () => {
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor])
|
||||
editor.registerCommand(
|
||||
INSERT_INLINE_BLOCK_COMMAND,
|
||||
(fields) => {
|
||||
if (targetNodeKey) {
|
||||
const node: InlineBlockNode = $getNodeByKey(targetNodeKey)
|
||||
|
||||
return <BlocksDrawerComponent />
|
||||
if (!node) {
|
||||
return false
|
||||
}
|
||||
node.setFields(fields as BlockFields)
|
||||
|
||||
setTargetNodeKey(null)
|
||||
return true
|
||||
}
|
||||
|
||||
const inlineBlockNode = $createInlineBlockNode(fields as BlockFields)
|
||||
$insertNodes([inlineBlockNode])
|
||||
if ($isRootOrShadowRoot(inlineBlockNode.getParentOrThrow())) {
|
||||
$wrapNodeInElement(inlineBlockNode, $createParagraphNode).selectEnd()
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
OPEN_INLINE_BLOCK_DRAWER_COMMAND,
|
||||
({ fields, nodeKey }) => {
|
||||
setBlockFields((fields as BlockFields) ?? null)
|
||||
setTargetNodeKey(nodeKey ?? null)
|
||||
setBlockType((fields as BlockFields)?.blockType ?? ('' as any))
|
||||
|
||||
if (nodeKey) {
|
||||
toggleModal(drawerSlug)
|
||||
return true
|
||||
}
|
||||
|
||||
let rangeSelection: RangeSelection | null = null
|
||||
|
||||
editor.getEditorState().read(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
rangeSelection = selection
|
||||
}
|
||||
})
|
||||
|
||||
if (rangeSelection) {
|
||||
//setLastSelection(rangeSelection)
|
||||
toggleModal(drawerSlug)
|
||||
}
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, targetNodeKey, toggleModal])
|
||||
|
||||
const blockDisplayName = reducedBlock?.labels?.singular
|
||||
? getTranslation(reducedBlock?.labels?.singular, i18n)
|
||||
: reducedBlock?.slug
|
||||
|
||||
return (
|
||||
<FieldsDrawer
|
||||
data={blockFields}
|
||||
drawerSlug={drawerSlug}
|
||||
drawerTitle={t(`lexical:blocks:inlineBlocks:${blockFields?.id ? 'edit' : 'create'}`, {
|
||||
label: blockDisplayName ?? t('lexical:blocks:inlineBlocks:label'),
|
||||
})}
|
||||
featureKey="blocks"
|
||||
handleDrawerSubmit={(_fields, data) => {
|
||||
closeModal(drawerSlug)
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
data.blockType = blockType
|
||||
|
||||
editor.dispatchCommand(INSERT_INLINE_BLOCK_COMMAND, data)
|
||||
}}
|
||||
schemaPathSuffix={blockFields?.blockType}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
|
||||
|
||||
import type { NodeValidation } from '../typesServer.js'
|
||||
import type { BlocksFeatureProps } from './feature.server.js'
|
||||
import type { BlockFields, SerializedBlockNode } from './nodes/BlocksNode.js'
|
||||
import type { SerializedInlineBlockNode } from './nodes/InlineBlocksNode.js'
|
||||
|
||||
export const blockValidationHOC = (
|
||||
props: BlocksFeatureProps,
|
||||
): NodeValidation<SerializedBlockNode> => {
|
||||
blocks: Block[],
|
||||
): NodeValidation<SerializedBlockNode | SerializedInlineBlockNode> => {
|
||||
return async ({ node, validation }) => {
|
||||
const blockFieldData = node.fields ?? ({} as BlockFields)
|
||||
|
||||
@@ -15,7 +17,7 @@ export const blockValidationHOC = (
|
||||
} = validation
|
||||
|
||||
// find block
|
||||
const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
|
||||
const block = blocks.find((block) => block.slug === blockFieldData.blockType)
|
||||
|
||||
// validate block
|
||||
if (!block) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.test-recorder-output {
|
||||
margin: 20px auto 20px auto;
|
||||
width: 100%;
|
||||
@@ -11,7 +13,7 @@
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
padding: 6px 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: $style-radius-m;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
border: 0;
|
||||
padding: 2px;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
border-radius: $style-radius-m;
|
||||
color: var(--theme-elevation-800);
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
@@ -38,7 +38,7 @@ html[data-theme='light'] {
|
||||
background: var(--color-base-0);
|
||||
min-width: 160px;
|
||||
color: var(--color-base-800);
|
||||
border-radius: 4px;
|
||||
border-radius: $style-radius-m;
|
||||
min-height: 40px;
|
||||
overflow-y: auto;
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
&__tableCellEditing {
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.4);
|
||||
border-radius: 3px;
|
||||
border-radius: $style-radius-m;
|
||||
}
|
||||
|
||||
&__tableAddColumns {
|
||||
|
||||
@@ -17,7 +17,7 @@ html[data-theme='light'] {
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
border-radius: 6.25px;
|
||||
border-radius: $style-radius-m;
|
||||
transition: opacity 0.2s;
|
||||
height: 37.5px;
|
||||
will-change: transform;
|
||||
@@ -69,7 +69,7 @@ html[data-theme='light'] {
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
color: var(--color-base-600);
|
||||
border-radius: 4px;
|
||||
border-radius: $style-radius-m;
|
||||
|
||||
&:hover:not([disabled]) {
|
||||
background-color: var(--color-base-100);
|
||||
|
||||
@@ -31,7 +31,6 @@ const initialParams = {
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
data: RelationshipData
|
||||
format?: ElementFormatType
|
||||
@@ -40,7 +39,6 @@ type Props = {
|
||||
|
||||
const Component: React.FC<Props> = (props) => {
|
||||
const {
|
||||
children,
|
||||
data: { relationTo, value: id },
|
||||
nodeKey,
|
||||
} = props
|
||||
@@ -189,7 +187,6 @@ const Component: React.FC<Props> = (props) => {
|
||||
)}
|
||||
|
||||
{id && <DocumentDrawer onSave={updateRelationship} />}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
align-items: center;
|
||||
background: var(--theme-input-bg);
|
||||
border: 1px solid var(--theme-elevation-100);
|
||||
border-radius: $style-radius-m;
|
||||
max-width: calc(var(--base) * 15);
|
||||
font-family: var(--font-body);
|
||||
margin: 0 0 1.5em;
|
||||
@@ -16,10 +17,6 @@
|
||||
border: 1px solid var(--theme-elevation-150);
|
||||
}
|
||||
|
||||
&[data-slate-node='element'] {
|
||||
margin: calc(var(--base) * 0.625) 0;
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin-bottom: calc(var(--base) * 0.25);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ html[data-theme='dark'] {
|
||||
z-index: 2;
|
||||
top: var(--doc-controls-height);
|
||||
border: $style-stroke-width-s solid var(--theme-elevation-150);
|
||||
// Make it so border itself is round too and not cut off at the corners
|
||||
border-collapse: unset;
|
||||
transform: translateY(1px); // aligns with top bar pixel line when stuck
|
||||
|
||||
&__group {
|
||||
|
||||
@@ -17,7 +17,7 @@ html[data-theme='light'] {
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
border-radius: 6.25px;
|
||||
border-radius: $style-radius-m;
|
||||
transition: opacity 0.2s;
|
||||
height: 37.5px;
|
||||
will-change: transform;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
width: 30px;
|
||||
border: 0;
|
||||
background: none;
|
||||
border-radius: 4px;
|
||||
border-radius: $style-radius-m;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
height: 30px;
|
||||
border: 0;
|
||||
background: none;
|
||||
border-radius: 4px;
|
||||
border-radius: $style-radius-m;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding: 0 10px;
|
||||
@@ -63,7 +63,7 @@
|
||||
&-items {
|
||||
position: absolute;
|
||||
background: var(--color-base-0);
|
||||
border-radius: 4px;
|
||||
border-radius: $style-radius-m;
|
||||
min-width: 132.5px;
|
||||
max-width: 200px;
|
||||
z-index: 100;
|
||||
@@ -88,7 +88,7 @@
|
||||
padding-right: 6.25px;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
border-radius: 4px;
|
||||
border-radius: $style-radius-m;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--theme-input-bg);
|
||||
border-radius: $style-radius-m;
|
||||
border: 1px solid var(--theme-elevation-100);
|
||||
position: relative;
|
||||
font-family: var(--font-body);
|
||||
@@ -20,10 +21,6 @@
|
||||
border: 1px solid var(--theme-elevation-150);
|
||||
}
|
||||
|
||||
&[data-slate-node='element'] {
|
||||
margin: calc(var(--base) * 0.625) 0;
|
||||
}
|
||||
|
||||
&__card {
|
||||
@include soft-shadow-bottom;
|
||||
display: flex;
|
||||
@@ -41,6 +38,7 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
border-top-left-radius: $style-radius-m;
|
||||
|
||||
img,
|
||||
svg {
|
||||
|
||||
@@ -812,16 +812,17 @@ export { AlignFeature } from './features/align/feature.server.js'
|
||||
export { BlockquoteFeature } from './features/blockquote/feature.server.js'
|
||||
export { BlocksFeature, type BlocksFeatureProps } from './features/blocks/feature.server.js'
|
||||
export { type BlockFields, BlockNode } from './features/blocks/nodes/BlocksNode.js'
|
||||
export { LinebreakHTMLConverter } from './features/converters/html/converter/converters/linebreak.js'
|
||||
|
||||
export { LinebreakHTMLConverter } from './features/converters/html/converter/converters/linebreak.js'
|
||||
export { ParagraphHTMLConverter } from './features/converters/html/converter/converters/paragraph.js'
|
||||
|
||||
export { TextHTMLConverter } from './features/converters/html/converter/converters/text.js'
|
||||
export { defaultHTMLConverters } from './features/converters/html/converter/defaultConverters.js'
|
||||
|
||||
export {
|
||||
convertLexicalNodesToHTML,
|
||||
convertLexicalToHTML,
|
||||
} from './features/converters/html/converter/index.js'
|
||||
|
||||
export type { HTMLConverter } from './features/converters/html/converter/types.js'
|
||||
export {
|
||||
HTMLConverterFeature,
|
||||
@@ -833,16 +834,16 @@ export { TreeViewFeature } from './features/debug/treeView/feature.server.js'
|
||||
export { EXPERIMENTAL_TableFeature } from './features/experimental_table/feature.server.js'
|
||||
export { BoldFeature } from './features/format/bold/feature.server.js'
|
||||
export { InlineCodeFeature } from './features/format/inlineCode/feature.server.js'
|
||||
|
||||
export { ItalicFeature } from './features/format/italic/feature.server.js'
|
||||
|
||||
export { StrikethroughFeature } from './features/format/strikethrough/feature.server.js'
|
||||
export { SubscriptFeature } from './features/format/subscript/feature.server.js'
|
||||
export { SuperscriptFeature } from './features/format/superscript/feature.server.js'
|
||||
export { UnderlineFeature } from './features/format/underline/feature.server.js'
|
||||
export { HeadingFeature, type HeadingFeatureProps } from './features/heading/feature.server.js'
|
||||
export { HorizontalRuleFeature } from './features/horizontalRule/feature.server.js'
|
||||
|
||||
export { IndentFeature } from './features/indent/feature.server.js'
|
||||
|
||||
export { LinkFeature, type LinkFeatureServerProps } from './features/link/feature.server.js'
|
||||
|
||||
export { AutoLinkNode } from './features/link/nodes/AutoLinkNode.js'
|
||||
|
||||
@@ -10,7 +10,7 @@ html[data-theme='light'] {
|
||||
background: var(--color-base-0);
|
||||
width: 200px;
|
||||
color: var(--color-base-800);
|
||||
border-radius: 4px;
|
||||
border-radius: $style-radius-m;
|
||||
list-style: none;
|
||||
font-family: var(--font-body);
|
||||
max-height: 300px;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.add-block-menu {
|
||||
all: unset; // reset all default button styles
|
||||
border-radius: 4px;
|
||||
border-radius: $style-radius-m;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.draggable-block-menu {
|
||||
border-radius: 4px;
|
||||
border-radius: $style-radius-m;
|
||||
padding: 0;
|
||||
cursor: grab;
|
||||
opacity: 0;
|
||||
@@ -50,7 +50,7 @@
|
||||
pointer-events: none;
|
||||
background: var(--theme-elevation-200);
|
||||
//border: 1px solid var(--theme-elevation-650);
|
||||
border-radius: 4px;
|
||||
border-radius: $style-radius-m;
|
||||
height: 50px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@@ -220,7 +220,7 @@ function useDraggableBlockMenu(
|
||||
blockHandleHorizontalOffset +
|
||||
(editorConfig?.admin?.hideGutter
|
||||
? menuRef?.current?.getBoundingClientRect()?.width ?? 0
|
||||
: -menuRef?.current?.getBoundingClientRect()?.width ?? 0),
|
||||
: -(menuRef?.current?.getBoundingClientRect()?.width ?? 0)),
|
||||
targetLineElem,
|
||||
targetBlockElem,
|
||||
lastTargetBlock,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import '../../scss/styles.scss';
|
||||
|
||||
.LexicalEditorTheme {
|
||||
&__ltr {
|
||||
text-align: left;
|
||||
@@ -229,17 +231,17 @@
|
||||
&__listItemUnchecked:focus:before,
|
||||
&__listItemChecked:focus:before {
|
||||
box-shadow: 0 0 0 2px #a6cdfe;
|
||||
border-radius: 2px;
|
||||
border-radius: $style-radius-m;
|
||||
}
|
||||
|
||||
&__listItemUnchecked:before {
|
||||
border: 1px solid #999;
|
||||
border-radius: 2px;
|
||||
border-radius: $style-radius-m;
|
||||
}
|
||||
|
||||
&__listItemChecked:before {
|
||||
border: 1px solid rgb(61, 135, 245);
|
||||
border-radius: 2px;
|
||||
border-radius: $style-radius-m;
|
||||
background-color: #3d87f5;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
|
||||
export const InlineBlocksIcon: React.FC = () => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="icon"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M5.33333 6.5C5.11232 6.5 4.90036 6.5878 4.74408 6.74408C4.5878 6.90036 4.5 7.11232 4.5 7.33333V12.1667C4.5 12.3877 4.5878 12.5996 4.74408 12.7559C4.90036 12.9122 5.11232 13 5.33333 13H14.6667C14.8877 13 15.0996 12.9122 15.2559 12.7559C15.4122 12.5996 15.5 12.3877 15.5 12.1667V11.6667C15.5 11.3905 15.7239 11.1667 16 11.1667C16.2761 11.1667 16.5 11.3905 16.5 11.6667V12.1667C16.5 12.6529 16.3068 13.1192 15.963 13.463C15.6192 13.8068 15.1529 14 14.6667 14H5.33333C4.8471 14 4.38079 13.8068 4.03697 13.463C3.69315 13.1192 3.5 12.6529 3.5 12.1667V7.33333C3.5 6.8471 3.69315 6.38079 4.03697 6.03697C4.38079 5.69315 4.8471 5.5 5.33333 5.5H10.3333C10.6095 5.5 10.8333 5.72386 10.8333 6C10.8333 6.27614 10.6095 6.5 10.3333 6.5H5.33333ZM13 6.5C12.7239 6.5 12.5 6.27614 12.5 6C12.5 5.72386 12.7239 5.5 13 5.5H16C16.2761 5.5 16.5 5.72386 16.5 6V9C16.5 9.27614 16.2761 9.5 16 9.5C15.7239 9.5 15.5 9.27614 15.5 9V7.20711L13.3536 9.35355C13.1583 9.54882 12.8417 9.54882 12.6464 9.35355C12.4512 9.15829 12.4512 8.84171 12.6464 8.64645L14.7929 6.5H13ZM6.16699 8.33325C6.16699 8.05711 6.39085 7.83325 6.66699 7.83325H11.0003C11.2765 7.83325 11.5003 8.05711 11.5003 8.33325C11.5003 8.60939 11.2765 8.83325 11.0003 8.83325H6.66699C6.39085 8.83325 6.16699 8.60939 6.16699 8.33325ZM6.16699 10.9999C6.16699 10.7238 6.39085 10.4999 6.66699 10.4999H13.3337C13.6098 10.4999 13.8337 10.7238 13.8337 10.9999C13.8337 11.2761 13.6098 11.4999 13.3337 11.4999H6.66699C6.39085 11.4999 6.16699 11.2761 6.16699 10.9999Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -79,6 +79,8 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
const LabelComponent = block?.LabelComponent
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`${parentPath.split('.').join('-')}-row-${rowIndex}`}
|
||||
@@ -119,19 +121,23 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
|
||||
: undefined
|
||||
}
|
||||
header={
|
||||
<div className={`${baseClass}__block-header`}>
|
||||
<span className={`${baseClass}__block-number`}>
|
||||
{String(rowIndex + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<Pill
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${row.blockType}`}
|
||||
pillStyle="white"
|
||||
>
|
||||
{getTranslation(block.labels.singular, i18n)}
|
||||
</Pill>
|
||||
<SectionTitle path={`${path}.blockName`} readOnly={readOnly} />
|
||||
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
|
||||
</div>
|
||||
LabelComponent ? (
|
||||
<LabelComponent blockKind={'block'} formData={row} />
|
||||
) : (
|
||||
<div className={`${baseClass}__block-header`}>
|
||||
<span className={`${baseClass}__block-number`}>
|
||||
{String(rowIndex + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<Pill
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${row.blockType}`}
|
||||
pillStyle="white"
|
||||
>
|
||||
{getTranslation(block.labels.singular, i18n)}
|
||||
</Pill>
|
||||
<SectionTitle path={`${path}.blockName`} readOnly={readOnly} />
|
||||
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
isCollapsed={row.collapsed}
|
||||
key={row.id}
|
||||
|
||||
@@ -315,6 +315,7 @@ export const mapFields = (args: {
|
||||
|
||||
const reducedBlock: ReducedBlock = {
|
||||
slug: block.slug,
|
||||
LabelComponent: block.admin?.components?.Label,
|
||||
custom: block.admin?.custom,
|
||||
fieldMap: blockFieldMap,
|
||||
imageAltText: block.imageAltText,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
Block,
|
||||
BlockField,
|
||||
CellComponentProps,
|
||||
FieldTypes,
|
||||
@@ -35,6 +36,7 @@ export type MappedTab = {
|
||||
}
|
||||
|
||||
export type ReducedBlock = {
|
||||
LabelComponent: Block['admin']['components']['Label']
|
||||
custom?: Record<any, string>
|
||||
fieldMap: FieldMap
|
||||
imageAltText?: string
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
|
||||
export const EmbedComponent: React.FC<any> = (props) => {
|
||||
const { data } = props
|
||||
|
||||
return <span>{data.key}</span>
|
||||
}
|
||||
10
test/fields/collections/Lexical/LabelComponent.tsx
Normal file
10
test/fields/collections/Lexical/LabelComponent.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export const LabelComponent: Block['admin']['components']['Label'] = (props) => {
|
||||
const { formData } = props
|
||||
return <div>{formData?.key}</div>
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { lexicalFieldsSlug } from '../../slugs.js'
|
||||
import { LabelComponent } from './LabelComponent.js'
|
||||
import {
|
||||
ConditionalLayoutBlock,
|
||||
RadioButtonsBlock,
|
||||
@@ -44,14 +45,14 @@ const editorConfig: ServerEditorConfig = {
|
||||
...defaultFields,
|
||||
{
|
||||
name: 'rel',
|
||||
label: 'Rel Attribute',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
||||
admin: {
|
||||
description:
|
||||
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
||||
},
|
||||
hasMany: true,
|
||||
label: 'Rel Attribute',
|
||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -81,6 +82,23 @@ const editorConfig: ServerEditorConfig = {
|
||||
ConditionalLayoutBlock,
|
||||
TabBlock,
|
||||
],
|
||||
inlineBlocks: [
|
||||
{
|
||||
slug: 'myInlineBlock',
|
||||
admin: {
|
||||
components: {
|
||||
Label: LabelComponent,
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
EXPERIMENTAL_TableFeature(),
|
||||
],
|
||||
@@ -88,13 +106,13 @@ const editorConfig: ServerEditorConfig = {
|
||||
|
||||
export const LexicalFields: CollectionConfig = {
|
||||
slug: lexicalFieldsSlug,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
listSearchableFields: ['title', 'richTextLexicalCustomFields'],
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
admin: {
|
||||
listSearchableFields: ['title', 'richTextLexicalCustomFields'],
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
@@ -128,13 +146,13 @@ export const LexicalFields: CollectionConfig = {
|
||||
{
|
||||
name: 'lexicalWithBlocks',
|
||||
type: 'richText',
|
||||
required: true,
|
||||
editor: lexicalEditor({
|
||||
admin: {
|
||||
hideGutter: false,
|
||||
},
|
||||
features: editorConfig.features,
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'lexicalWithBlocks_markdown',
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": [
|
||||
"./test/_community/config.ts"
|
||||
"./test/fields/config.ts"
|
||||
],
|
||||
"@payloadcms/live-preview": [
|
||||
"./packages/live-preview/src"
|
||||
|
||||
Reference in New Issue
Block a user