Components
Media Popover

Media Popover

Access media-related features and options through a popover.

📸 Image

Add images by either uploading them or providing the image URL:

Customize image captions and resize images.

📺 Embed

Embed various types of content, such as videos and tweets:




Installation

npx @udecode/plate-ui@latest add media-popover

Examples

import React from 'react';
import {
  Box,
  PlateElement,
  PlateElementProps,
  Value,
} from '@udecode/plate-common';
import {
  Caption,
  CaptionTextarea,
  ELEMENT_IMAGE,
  Image,
  Resizable,
  TImageElement,
  useMediaState,
} from '@udecode/plate-media';
 
import { cn } from '@/lib/utils';
 
import { MediaPopover } from './media-popover';
 
const align = 'center';
 
export function ImageElement({
  className,
  children,
  nodeProps,
  ...props
}: PlateElementProps<Value, TImageElement>) {
  const { readOnly, focused, selected } = useMediaState();
 
  return (
    <MediaPopover pluginKey={ELEMENT_IMAGE}>
      <PlateElement className={cn('py-2.5', className)} {...props}>
        <figure className="group relative m-0" contentEditable={false}>
          <Resizable
            className={cn(align === 'center' && 'mx-auto')}
            options={{
              renderHandleLeft: (htmlProps) => (
                <Box
                  {...htmlProps}
                  className={cn(
                    'absolute top-0 z-10 flex h-full w-6 select-none flex-col justify-center',
                    'after:flex after:h-16 after:bg-ring after:opacity-0 after:group-hover:opacity-100',
                    "after:w-[3px] after:rounded-[6px] after:content-['_']",
                    focused && selected && 'opacity-100',
                    // variant left
                    '-left-3 -ml-3 pl-3'
                  )}
                />
              ),
              renderHandleRight: (htmlProps) => (
                <Box
                  {...htmlProps}
                  className={cn(
                    'absolute top-0 z-10 flex h-full w-6 select-none flex-col justify-center',
                    'after:flex after:h-16 after:bg-ring after:opacity-0 after:group-hover:opacity-100',
                    "after:w-[3px] after:rounded-[6px] after:content-['_']",
                    focused && selected && 'opacity-100',
                    // variant right
                    '-right-3 -mr-3 items-end pr-3'
                  )}
                />
              ),
              align,
              readOnly,
            }}
          >
            {/* eslint-disable-next-line jsx-a11y/alt-text */}
            <Image
              {...nodeProps}
              className={cn(
                'block w-full max-w-full cursor-pointer object-cover px-0',
                'rounded-sm',
                focused && selected && 'ring-2 ring-ring ring-offset-2',
                nodeProps?.className
              )}
            />
          </Resizable>
 
          <Caption
            className={cn('max-w-full', align === 'center' && 'mx-auto')}
          >
            <CaptionTextarea
              className={cn(
                'mt-2 w-full resize-none border-none bg-inherit p-0 font-[inherit] text-inherit',
                'focus:outline-none focus:[&::placeholder]:opacity-0',
                'text-center'
              )}
              placeholder="Write a caption..."
              readOnly={readOnly}
            />
          </Caption>
        </figure>
 
        {children}
      </PlateElement>
    </MediaPopover>
  );
}
import React from 'react';
import {
  Box,
  PlateElement,
  PlateElementProps,
  Value,
} from '@udecode/plate-common';
import {
  Caption,
  CaptionTextarea,
  ELEMENT_MEDIA_EMBED,
  parseTwitterUrl,
  parseVideoUrl,
  Resizable,
  TMediaEmbedElement,
  useMediaState,
} from '@udecode/plate-media';
import { cva } from 'class-variance-authority';
import LiteYouTubeEmbed from 'react-lite-youtube-embed';
 
import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css';
 
import { Tweet } from 'react-tweet';
 
import { cn } from '@/lib/utils';
 
import { MediaPopover } from './media-popover';
 
export const handleVariants = cva(
  cn(
    'absolute top-0 z-10 flex h-full w-6 select-none flex-col justify-center',
    'after:flex after:h-16 after:bg-ring after:opacity-0 group-hover:after:opacity-100',
    "after:w-[3px] after:rounded-[6px] after:content-['_']"
  ),
  {
    variants: {
      placement: {
        left: '-left-3 -ml-3 pl-3',
        right: '-right-3 -mr-3 items-end pr-3',
      },
    },
  }
);
 
const MediaEmbedElement = React.forwardRef<
  React.ElementRef<typeof PlateElement>,
  PlateElementProps<Value, TMediaEmbedElement>
>(({ className, children, ...props }, ref) => {
  const { focused, readOnly, selected, embed, isTweet, isYoutube, isVideo } =
    useMediaState({
      urlParsers: [parseTwitterUrl, parseVideoUrl],
    });
  const provider = embed?.provider;
 
  return (
    <MediaPopover pluginKey={ELEMENT_MEDIA_EMBED}>
      <PlateElement
        ref={ref}
        className={cn('relative py-2.5', className)}
        {...props}
      >
        <figure className="group relative m-0 w-full" contentEditable={false}>
          <Resizable
            className={cn('mx-auto')}
            options={{
              maxWidth: isTweet ? 550 : '100%',
              minWidth: isTweet ? 300 : 100,
              renderHandleLeft: (htmlProps) => (
                <Box
                  {...htmlProps}
                  className={handleVariants({
                    placement: 'left',
                  })}
                />
              ),
              renderHandleRight: (htmlProps) => (
                <Box
                  {...htmlProps}
                  className={handleVariants({
                    placement: 'right',
                  })}
                />
              ),
            }}
          >
            {isYoutube && <LiteYouTubeEmbed id={embed!.id!} title="youtube" />}
            {isVideo && !isYoutube && (
              <div
                className={cn(
                  provider === 'youtube' && 'pb-[56.2061%]',
                  provider === 'vimeo' && 'pb-[75%]',
                  provider === 'youku' && 'pb-[56.25%]',
                  provider === 'dailymotion' && 'pb-[56.0417%]',
                  provider === 'coub' && 'pb-[51.25%]'
                )}
              >
                <iframe
                  className={cn(
                    'absolute left-0 top-0 h-full w-full rounded-sm',
                    isVideo && 'border-0',
                    focused && selected && 'ring-2 ring-ring ring-offset-2'
                  )}
                  src={embed!.url}
                  title="embed"
                  allowFullScreen
                />
              </div>
            )}
            {isTweet && (
              <div
                className={cn(
                  '[&_.react-tweet-theme]:my-0',
                  !readOnly &&
                    selected &&
                    '[&_.react-tweet-theme]:ring-2 [&_.react-tweet-theme]:ring-ring [&_.react-tweet-theme]:ring-offset-2'
                )}
              >
                <Tweet id={embed!.id!} />
              </div>
            )}
          </Resizable>
 
          <Caption className={cn('mx-auto')}>
            <CaptionTextarea
              className={cn(
                'mt-2 w-full resize-none border-none bg-inherit p-0 font-[inherit] text-inherit',
                'focus:outline-none focus:[&::placeholder]:opacity-0',
                'text-center'
              )}
              placeholder="Write a caption..."
            />
          </Caption>
        </figure>
 
        {children}
      </PlateElement>
    </MediaPopover>
  );
});
MediaEmbedElement.displayName = 'MediaEmbedElement';
 
export { MediaEmbedElement };