Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
89 changes: 89 additions & 0 deletions src/blocks/InlineMedia/Component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { InlineMediaBlock } from '@/payload-types'

import { Media } from '@/components/Media'
import { cn } from '@/utilities/ui'

type Props = Omit<InlineMediaBlock, 'blockType' | 'id'>

const widthClasses = {
'25': 'w-1/4',
'50': 'w-1/2',
'75': 'w-3/4',
'100': 'w-full',
}

const verticalAlignClasses = {
top: 'align-top',
middle: 'align-middle',
bottom: 'align-bottom',
baseline: 'align-baseline',
}

type WidthSize = keyof typeof widthClasses

function isWidthSize(size: string): size is WidthSize {
return size in widthClasses
}

export const InlineMediaComponent = ({
media,
position = 'inline',
verticalAlign = 'middle',
size = 'original',
fixedHeight,
caption,
}: Props) => {
if (!media || typeof media === 'number' || typeof media === 'string') {
return null
}

const isFloat = position === 'float-left' || position === 'float-right'
const resolvedSize = size ?? 'original'

let sizeClass = ''
let imgSizeClass = 'w-auto h-auto'
let sizes = '100vw'
const isFixedHeight = resolvedSize === 'fixed-height' && fixedHeight

if (resolvedSize === 'original') {
sizeClass = 'max-w-fit'
} else if (isWidthSize(resolvedSize)) {
sizeClass = widthClasses[resolvedSize]
imgSizeClass = 'w-full h-auto'
// Approximate sizes hint for responsive images
sizes = `${resolvedSize}vw`
} else if (isFixedHeight) {
imgSizeClass = 'h-full w-auto'
sizes = '96px'
}

const positionClasses = isFloat
? cn(position === 'float-left' ? 'float-left mr-2' : 'float-right ml-2', 'mb-1')
: cn('inline-block', verticalAlignClasses[verticalAlign ?? 'middle'])

// For fixed height, wrap Media in a span with explicit height.
// Descendant selectors propagate height through Media's intermediate span and picture elements.
const mediaElement = isFixedHeight ? (
<span
className="block [&>span]:h-full [&_picture]:h-full [&_picture]:my-0"
style={{ height: `${fixedHeight}px` }}
>
<Media htmlElement="span" resource={media} imgClassName={imgSizeClass} sizes={sizes} />
</span>
) : (
<Media
htmlElement="span"
resource={media}
imgClassName={imgSizeClass}
pictureClassName="my-0"
sizes={sizes}
/>
)

return (
<span className={cn(positionClasses, sizeClass)}>
{mediaElement}
{caption && <span className="block text-xs text-gray-500 mt-0.5">{caption}</span>}
</span>
)
}
76 changes: 76 additions & 0 deletions src/blocks/InlineMedia/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { Block } from 'payload'

export const InlineMediaBlock: Block = {
slug: 'inlineMedia',
interfaceName: 'InlineMediaBlock',
fields: [
{
name: 'media',
type: 'upload',
relationTo: 'media',
required: true,
},
{
name: 'position',
type: 'select',
defaultValue: 'inline',
options: [
{ label: 'Inline', value: 'inline' },
{ label: 'Float left', value: 'float-left' },
{ label: 'Float right', value: 'float-right' },
],
admin: {
description:
'Inline renders the image within the text flow. Float positions the image to one side with text wrapping around it.',
},
},
{
name: 'verticalAlign',
type: 'select',
defaultValue: 'middle',
options: [
{ label: 'Middle', value: 'middle' },
{ label: 'Top', value: 'top' },
{ label: 'Bottom', value: 'bottom' },
{ label: 'Baseline', value: 'baseline' },
],
admin: {
description: 'Vertical alignment relative to the surrounding text.',
condition: (_, siblingData) => siblingData?.position === 'inline',
},
},
{
name: 'size',
type: 'select',
defaultValue: 'original',
options: [
{ label: 'Original (natural size)', value: 'original' },
{ label: '25% width', value: '25' },
{ label: '50% width', value: '50' },
{ label: '75% width', value: '75' },
{ label: '100% width', value: '100' },
{ label: 'Fixed height', value: 'fixed-height' },
],
admin: {
description:
'Original uses the natural image size. Percentage widths are relative to the containing block. Fixed height lets you specify an exact pixel height.',
},
},
{
name: 'fixedHeight',
type: 'number',
min: 1,
admin: {
description: 'Height in pixels.',
condition: (_, siblingData) => siblingData?.size === 'fixed-height',
},
},
{
name: 'caption',
type: 'text',
admin: {
description: 'Optional text shown as a tooltip on hover.',
},
},
],
}
2 changes: 2 additions & 0 deletions src/collections/Posts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EventListBlock } from '@/blocks/EventList/config'
import { EventTableBlock } from '@/blocks/EventTable/config'
import { GenericEmbedBlock } from '@/blocks/GenericEmbed/config'
import { HeaderLexicalBlock } from '@/blocks/Header/config'
import { InlineMediaBlock } from '@/blocks/InlineMedia/config'
import { MediaBlock } from '@/blocks/Media/config'
import { SingleBlogPostBlock } from '@/blocks/SingleBlogPost/config'
import { SingleEventBlock } from '@/blocks/SingleEvent/config'
Expand Down Expand Up @@ -102,6 +103,7 @@ export const Posts: CollectionConfig<'posts'> = {
SingleEventBlock,
SponsorsBlock,
],
inlineBlocks: [InlineMediaBlock],
}),
HorizontalRuleFeature(),
InlineToolbarFeature(),
Expand Down
7 changes: 7 additions & 0 deletions src/components/RichText/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { MediaBlockComponent } from '@/blocks/Media/Component'
import {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
SerializedLinkNode,
} from '@payloadcms/richtext-lexical'
import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
Expand All @@ -19,6 +20,7 @@ import { EventListBlockComponent } from '@/blocks/EventList/Component'
import { EventTableBlockComponent } from '@/blocks/EventTable/Component'
import { GenericEmbedBlockComponent } from '@/blocks/GenericEmbed/Component'
import { HeaderBlockComponent } from '@/blocks/Header/Component'
import { InlineMediaComponent } from '@/blocks/InlineMedia/Component'
import { SingleBlogPostBlockComponent } from '@/blocks/SingleBlogPost/Component'
import { SingleEventBlockComponent } from '@/blocks/SingleEvent/Component'
import { SponsorsBlockComponent } from '@/blocks/Sponsors/components'
Expand All @@ -33,6 +35,7 @@ import type {
EventTableBlock as EventTableBlockProps,
GenericEmbedBlock as GenericEmbedBlockProps,
HeaderBlock as HeaderBlockProps,
InlineMediaBlock as InlineMediaBlockProps,
MediaBlock as MediaBlockProps,
Page,
Post,
Expand Down Expand Up @@ -84,6 +87,7 @@ type NodeTypes =
| SingleEventBlockProps
| SponsorsBlockProps
>
| SerializedInlineBlockNode<InlineMediaBlockProps>

const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
const { linkType, doc, url } = linkNode.fields
Expand Down Expand Up @@ -136,6 +140,9 @@ const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters })
),
sponsorsBlock: ({ node }) => <SponsorsBlockComponent {...node.fields} isLayoutBlock={false} />,
},
inlineBlocks: {
inlineMedia: ({ node }) => <InlineMediaComponent {...node.fields} />,
},
})

type Props = {
Expand Down
2 changes: 2 additions & 0 deletions src/fields/defaultLexical.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BlogListBlock } from '@/blocks/BlogList/config'
import { GenericEmbedBlock } from '@/blocks/GenericEmbed/config'
import { InlineMediaBlock } from '@/blocks/InlineMedia/config'
import { SingleBlogPostBlock } from '@/blocks/SingleBlogPost/config'
import { getTenantFilter } from '@/utilities/collectionFilters'
import { validateExternalUrl } from '@/utilities/validateUrl'
Expand Down Expand Up @@ -67,6 +68,7 @@ export const defaultLexical: Config['editor'] = lexicalEditor({
}),
BlocksFeature({
blocks: [GenericEmbedBlock, BlogListBlock, SingleBlogPostBlock],
inlineBlocks: [InlineMediaBlock],
}),
FixedToolbarFeature(),
OrderedListFeature(),
Expand Down
30 changes: 30 additions & 0 deletions src/payload-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4843,6 +4843,36 @@ export interface SingleEventBlock {
blockName?: string | null;
blockType: 'singleEvent';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "InlineMediaBlock".
*/
export interface InlineMediaBlock {
media: number | Media;
/**
* Inline renders the image within the text flow. Float positions the image to one side with text wrapping around it.
*/
position?: ('inline' | 'float-left' | 'float-right') | null;
/**
* Vertical alignment relative to the surrounding text.
*/
verticalAlign?: ('middle' | 'top' | 'bottom' | 'baseline') | null;
/**
* Original uses the natural image size. Percentage widths are relative to the containing block. Fixed height lets you specify an exact pixel height.
*/
size?: ('original' | '25' | '50' | '75' | '100' | 'fixed-height') | null;
/**
* Height in pixels.
*/
fixedHeight?: number | null;
/**
* Optional text shown as a tooltip on hover.
*/
caption?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'inlineMedia';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
Expand Down