-
Notifications
You must be signed in to change notification settings - Fork 1
Inline media block #968
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Inline media block #968
Changes from all commits
5835212
170000e
dd16847
47ad611
1903e72
1c36201
ac35d98
1827a03
dfe299f
61ba101
4b352f3
dd99700
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| 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" | ||
| style={{ height: `${fixedHeight}px` }} | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⛏️ You could make this a class name like you are above and remove the inline style.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| > | ||
| <Media | ||
| htmlElement="span" | ||
| resource={media} | ||
| imgClassName={imgSizeClass} | ||
| pictureClassName="my-0" | ||
| 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> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import type { Block } from 'payload' | ||
|
|
||
| export const InlineMediaBlock: Block = { | ||
| slug: 'inlineMedia', | ||
| interfaceName: 'InlineMediaBlock', | ||
| imageURL: '/thumbnail/MediaThumbnail.jpg', | ||
| fields: [ | ||
| { | ||
| name: 'media', | ||
| type: 'upload', | ||
| relationTo: 'media', | ||
| required: true, | ||
| }, | ||
| { | ||
| name: 'position', | ||
| type: 'select', | ||
| defaultValue: 'inline', | ||
| options: [ | ||
| { label: 'Inline', value: 'inline' }, | ||
| { label: 'Left', value: 'float-left' }, | ||
| { label: 'Right', value: 'float-right' }, | ||
| ], | ||
| admin: { | ||
| description: | ||
| 'Inline renders the image within the text flow. Left or Right positions the image to that 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' }, | ||
|
Comment on lines
+49
to
+52
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it matters, but maybe we add a
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the idea, to make it clear that these are strings but we actually use these for the sizes attribute in the component so we'd have to strip off the 'w' prefix which I think would be more confusing. |
||
| { 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.', | ||
| }, | ||
| }, | ||
| ], | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { InlineMediaBlock } from '@/blocks/InlineMedia/config' | ||
|
|
||
| export const DEFAULT_INLINE_BLOCKS = [InlineMediaBlock] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this being calculated/ is it needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the
attribute — a hint that tells the browser which source to pick from a responsive srcset before layout is known. For fixed-height images the rendered width depends on the image's aspect ratio, which we don't have at render time without reading the media dimensions. 96px is a conservative fallback so the browser doesn't load the full-resolution source. It's imperfect but errs on the side of a smaller download; the browser will still render at the correct size.