Loading...
Files
components/demo.tsx
'use client';

import * as React from 'react';

import { Plate, usePlateEditor } from 'platejs/react';

import { EditorKit } from '@/components/editor/editor-kit';
import { Editor, EditorContainer } from '@/components/ui/editor';

import { DEMO_VALUES } from './values/demo-values';

export default function Demo({ id }: { id: string }) {
  const editor = usePlateEditor({
    plugins: EditorKit,
    value: DEMO_VALUES[id],
  });

  return (
    <Plate editor={editor}>
      <EditorContainer variant="demo">
        <Editor />
      </EditorContainer>
    </Plate>
  );
}

功能特性

媒体支持

  • 文件类型:
    • 图片
    • 视频
    • 音频
    • 其他 (PDF, Word等)
  • 视频平台:
    • 本地视频文件
    • YouTube, Vimeo, Dailymotion, Youku, Coub
  • 嵌入支持:
    • 推特推文

媒体功能

  • 可编辑的标题说明
  • 可调整大小的元素

上传功能

  • 多种上传方式:
    • 工具栏按钮文件选择器
    • 从文件系统拖放
    • 从剪贴板粘贴(图片)
    • 外部媒体URL嵌入
  • 上传体验:
    • 实时进度跟踪
    • 上传过程中预览
    • 上传或嵌入完成后自动将占位符转换为相应的媒体元素(图片/视频/音频/文件)
    • 错误处理
    • 文件大小验证
    • 类型验证

套件使用

安装

最快捷的媒体支持方式是使用MediaKit,它包含预配置的ImagePluginVideoPluginAudioPluginFilePluginMediaEmbedPluginPlaceholderPluginCaptionPlugin及其Plate UI组件。

'use client';
 
import { CaptionPlugin } from '@platejs/caption/react';
import {
  AudioPlugin,
  FilePlugin,
  ImagePlugin,
  MediaEmbedPlugin,
  PlaceholderPlugin,
  VideoPlugin,
} from '@platejs/media/react';
import { KEYS } from 'platejs';
 
import { AudioElement } from '@/components/ui/media-audio-node';
import { MediaEmbedElement } from '@/components/ui/media-embed-node';
import { FileElement } from '@/components/ui/media-file-node';
import { ImageElement } from '@/components/ui/media-image-node';
import { PlaceholderElement } from '@/components/ui/media-placeholder-node';
import { MediaPreviewDialog } from '@/components/ui/media-preview-dialog';
import { MediaUploadToast } from '@/components/ui/media-upload-toast';
import { VideoElement } from '@/components/ui/media-video-node';
 
export const MediaKit = [
  ImagePlugin.configure({
    options: { disableUploadInsert: true },
    render: { afterEditable: MediaPreviewDialog, node: ImageElement },
  }),
  MediaEmbedPlugin.withComponent(MediaEmbedElement),
  VideoPlugin.withComponent(VideoElement),
  AudioPlugin.withComponent(AudioElement),
  FilePlugin.withComponent(FileElement),
  PlaceholderPlugin.configure({
    options: { disableEmptyPlaceholder: true },
    render: { afterEditable: MediaUploadToast, node: PlaceholderElement },
  }),
  CaptionPlugin.configure({
    options: {
      query: {
        allow: [KEYS.img, KEYS.video, KEYS.audio, KEYS.file, KEYS.mediaEmbed],
      },
    },
  }),
];

添加套件

将套件添加到你的插件中:

import { createPlateEditor } from 'platejs/react';
import { MediaKit } from '@/components/editor/plugins/media-kit';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件
    ...MediaKit,
  ],
});

添加API路由

npx shadcn@latest add https://platejs.org/r/media-uploadthing-api

环境配置

UploadThing获取密钥并添加到.env:

.env
UPLOADTHING_TOKEN=xxx

手动使用

安装

pnpm add @platejs/media

添加插件

在创建编辑器时将媒体插件包含到Plate插件数组中。

import {
  AudioPlugin,
  FilePlugin,
  ImagePlugin,
  MediaEmbedPlugin,
  PlaceholderPlugin,
  VideoPlugin,
} from '@platejs/media/react';
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件
    ImagePlugin,
    VideoPlugin,
    AudioPlugin,
    FilePlugin,
    MediaEmbedPlugin,
    PlaceholderPlugin,
  ],
});

配置插件

使用自定义组件和上传设置配置插件。

import {
  AudioPlugin,
  FilePlugin,
  ImagePlugin,
  MediaEmbedPlugin,
  PlaceholderPlugin,
  VideoPlugin,
} from '@platejs/media/react';
import { KEYS } from 'platejs';
import { createPlateEditor } from 'platejs/react';
import { 
  AudioElement, 
  FileElement, 
  ImageElement, 
  MediaEmbedElement, 
  PlaceholderElement, 
  VideoElement 
} from '@/components/ui/media-nodes';
import { MediaUploadToast } from '@/components/ui/media-upload-toast';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件
    ImagePlugin.withComponent(ImageElement),
    VideoPlugin.withComponent(VideoElement),
    AudioPlugin.withComponent(AudioElement),
    FilePlugin.withComponent(FileElement),
    MediaEmbedPlugin.withComponent(MediaEmbedElement),
    PlaceholderPlugin.configure({
      options: { disableEmptyPlaceholder: true },
      render: { afterEditable: MediaUploadToast, node: PlaceholderElement },
    }),
  ],
});
  • withComponent: 为每种媒体类型分配自定义渲染组件
  • options.disableEmptyPlaceholder: 无文件上传时禁止显示占位符
  • render.afterEditable: 在编辑器外部渲染上传进度提示

标题支持

要启用媒体标题,添加Caption Plugin:

import { CaptionPlugin } from '@platejs/caption/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件
    // ...媒体插件
    CaptionPlugin.configure({
      options: {
        query: {
          allow: [KEYS.img, KEYS.video, KEYS.audio, KEYS.file, KEYS.mediaEmbed],
        },
      },
    }),
  ],
});

自定义上传实现

对于自定义上传实现,创建符合此接口的上传钩子:

interface UseUploadFileProps {
  onUploadComplete?: (file: UploadedFile) => void;
  onUploadError?: (error: unknown) => void;
  headers?: Record<string, string>;
  onUploadBegin?: (fileName: string) => void;
  onUploadProgress?: (progress: { progress: number }) => void;
  skipPolling?: boolean;
}
 
interface UploadedFile {
  key: string;    // 唯一标识符
  url: string;    // 上传文件的公开URL
  name: string;   // 原始文件名
  size: number;   // 文件大小(字节)
  type: string;   // MIME类型
}

使用S3预签名URL的示例实现:

export function useUploadFile({ 
  onUploadComplete, 
  onUploadError, 
  onUploadProgress 
}: UseUploadFileProps = {}) {
  const [uploadedFile, setUploadedFile] = useState<UploadedFile>();
  const [uploadingFile, setUploadingFile] = useState<File>();
  const [progress, setProgress] = useState(0);
  const [isUploading, setIsUploading] = useState(false);
 
  async function uploadFile(file: File) {
    setIsUploading(true);
    setUploadingFile(file);
 
    try {
      // 从后端获取预签名URL和最终URL
      const { presignedUrl, fileUrl, fileKey } = await fetch('/api/upload', {
        method: 'POST',
        body: JSON.stringify({
          filename: file.name,
          contentType: file.type,
        }),
      }).then(r => r.json());
 
      // 使用预签名URL上传到S3
      await axios.put(presignedUrl, file, {
        headers: { 'Content-Type': file.type },
        onUploadProgress: (progressEvent) => {
          const progress = (progressEvent.loaded / progressEvent.total) * 100;
          setProgress(progress);
          onUploadProgress?.({ progress });
        },
      });
 
      const uploadedFile = {
        key: fileKey,
        url: fileUrl,
        name: file.name,
        size: file.size,
        type: file.type,
      };
 
      setUploadedFile(uploadedFile);
      onUploadComplete?.(uploadedFile);
      
      return uploadedFile;
    } catch (error) {
      onUploadError?.(error);
      throw error;
    } finally {
      setProgress(0);
      setIsUploading(false);
      setUploadingFile(undefined);
    }
  }
 
  return {
    isUploading,
    progress,
    uploadFile,
    uploadedFile,
    uploadingFile,
  };
}

然后将你的自定义上传钩子与媒体组件集成:

import { useUploadFile } from '@/hooks/use-upload-file'; // 你的自定义钩子
 
// 在你的PlaceholderElement组件中
export function PlaceholderElement({ className, children, element, ...props }) {
  const { uploadFile, isUploading, progress } = useUploadFile({
    onUploadComplete: (uploadedFile) => {
      // 将占位符替换为实际媒体元素
      const { url, type } = uploadedFile;
      
      // 将占位符转换为适当的媒体类型
      editor.tf.replace.placeholder({
        id: element.id,
        url,
        type: getMediaType(type), // image, video, audio, file
      });
    },
    onUploadError: (error) => {
      console.error('上传失败:', error);
      // 处理上传错误,可能显示提示
    },
  });
 
  // 当文件被拖放或选择时使用uploadFile
  // 这与PlaceholderPlugin的文件处理集成
}

添加工具栏按钮

你可以将MediaToolbarButton添加到Toolbar以上传和插入媒体。

插入工具栏按钮

你可以将这些项添加到插入工具栏按钮来插入媒体元素:

{
  icon: <ImageIcon />,
  label: '图片',
  value: KEYS.img,
}

Plate Plus

  • Integration with UploadThing
  • Use slash commands for quick insertion
  • Displays clickable placeholders for various media types (image, video, audio, file)
  • Opens a popover with two tabs when the placeholder is clicked:
    • Upload tab: Allows uploading local files directly
    • Embed tab: Enables pasting embed links for media content
  • Image-specific features:
    • Better loading rendering and image replacement
    • Alignment options
    • Expand/collapse view
    • Download button
  • Video-specific features:
    • Lazy load
    • Alignment options
    • Caption support
    • View original source
  • Floating toolbar appears at the top right of media elements:
    • Alignment dropdown menu
    • Caption button
    • Expand button
    • Download button
  • Beautifully crafted UI

插件

ImagePlugin

用于void图片元素的插件。

OptionsImagePluginOptions

Collapse all

    上传图片到服务器的函数。接收:

    • 来自FileReader.readAsDataURL的数据URL(字符串)
    • 来自剪贴板数据的ArrayBuffer 返回:
    • 上传图片的URL字符串
    • 如果不需要上传则返回原始数据URL/ArrayBuffer
    • 默认: 返回原始输入

    禁用数据插入时的文件上传。

    • 默认: false

    禁用数据插入时的URL嵌入。

    • 默认: false

    检查文本字符串是否为URL的函数。

    转换URL的函数。

VideoPlugin

用于void视频元素的插件。扩展MediaPluginOptions

AudioPlugin

用于void音频元素的插件。扩展MediaPluginOptions

FilePlugin

用于void文件元素的插件。扩展MediaPluginOptions

MediaEmbedPlugin

用于void媒体嵌入元素的插件。扩展MediaPluginOptions

PlaceholderPlugin

管理上传过程中媒体占位符的插件。处理文件上传、拖放和剪贴板粘贴事件。

Optionsobject

Collapse all

    不同文件类型的配置。默认配置:

    {
      audio: {
        maxFileCount: 1,
        maxFileSize: '8MB',
        mediaType: KEYS.audio,
        minFileCount: 1,
      },
      blob: {
        maxFileCount: 1,
        maxFileSize: '8MB',
        mediaType: KEYS.file,
        minFileCount: 1,
      },
      image: {
        maxFileCount: 3,
        maxFileSize: '4MB',
        mediaType: KEYS.image,
        minFileCount: 1,
      },
      pdf: {
        maxFileCount: 1,
        maxFileSize: '4MB',
        mediaType: KEYS.file,
        minFileCount: 1,
      },
      text: {
        maxFileCount: 1,
        maxFileSize: '64KB',
        mediaType: KEYS.file,
        minFileCount: 1,
      },
      video: {
        maxFileCount: 1,
        maxFileSize: '16MB',
        mediaType: KEYS.video,
        minFileCount: 1,
      },
    }

    支持的文件类型: 'image' | 'video' | 'audio' | 'pdf' | 'text' | 'blob'

    无文件上传时禁用空占位符。

    • 默认: false

    禁用拖放文件上传功能。

    • 默认: false

    如果uploadConfig未指定,可一次上传的最大文件数。

    • 默认: 5

    允许上传多个相同类型的文件。

    • 默认: true

API

api.placeholder.addUploadingFile

跟踪当前正在上传的文件。

Parameters

Collapse all

    占位符元素的唯一标识符。

    正在上传的文件。

api.placeholder.getUploadingFile

获取当前正在上传的文件。

Parameters

Collapse all

    占位符元素的唯一标识符。

Returns

Collapse all

    如果找到则返回上传文件,否则返回undefined。

api.placeholder.removeUploadingFile

上传完成或失败后从上传跟踪状态中移除文件。

Parameters

Collapse all

    要移除的占位符元素的唯一标识符。

转换方法

tf.insert.media

使用上传占位符将媒体文件插入编辑器。

Parameters

Collapse all

    要上传的文件。根据配置的文件类型和限制进行验证。

    插入节点的转换选项。

Optionsobject

Collapse all

    插入媒体的位置。默认为当前选区。

    是否在媒体后插入新块。

    • 默认: true

根据配置的限制(大小、数量、类型)验证文件,为每个文件创建占位符元素,处理多个文件顺序上传,维护撤销/重做操作的上传历史记录,如果验证失败则触发错误处理。

错误代码:

enum UploadErrorCode {
  INVALID_FILE_TYPE = 400,
  TOO_MANY_FILES = 402,
  INVALID_FILE_SIZE = 403,
  TOO_LESS_FILES = 405,
  TOO_LARGE = 413,
}

tf.insert.imagePlaceholder

插入一个在上传完成后转换为图片元素的占位符。

tf.insert.videoPlaceholder

插入一个在上传完成后转换为视频元素的占位符。

tf.insert.audioPlaceholder

插入一个在上传完成后转换为音频元素的占位符。

tf.insert.filePlaceholder

插入一个在上传完成后转换为文件元素的占位符。

tf.insert.image

在编辑器中插入图片元素。

Parameters

Collapse all

    图片的 URL 或 ArrayBuffer。

    插入图片元素的额外选项。

OptionsInsertImageOptions

Collapse all

    如果为 true,图片将被插入到下一个块中。

tf.insert.mediaEmbed

在当前选区插入媒体嵌入元素。

OptionsInsertMediaEmbedOptions

Collapse all

    媒体嵌入的 URL。

    • 默认值: ''

    媒体嵌入元素的键。

    • 默认值: KEYS.mediaEmbed

    插入节点的额外选项。

Hooks

useResizable

处理媒体元素的可调整大小属性。

State

Collapse all

    可调整大小元素内容的对齐方式。

    可调整大小元素可以调整到的最小宽度。

    可调整大小元素可以调整到的最大宽度。

    调整大小时设置节点宽度的函数。

    直接设置可调整大小元素宽度的函数。

    可调整大小元素的当前宽度(百分比、'auto' 或像素)。

Returnsobject

Collapse all

    最外层包装 div 的 React 引用。

    包装 div 的 CSS 样式。

    可调整大小元素的 CSS 样式。

    元素调整大小时调用的回调函数。

useMediaState

媒体元素的状态钩子。

Parameters

Collapse all

    用于解析媒体元素 URL 的 URL 解析器数组。

    • EmbedUrlParser: (url: string) => EmbedUrlData | undefined

Returnsobject

Collapse all

    媒体元素的对齐方式。

    媒体元素是否当前获得焦点。

    媒体元素是否当前被选中。

    编辑器是否处于只读模式。

    媒体元素的解析嵌入数据。

    媒体元素是否为推文。

    媒体元素是否为视频。

    媒体元素是否为 YouTube 视频。

useMediaToolbarButton

媒体工具栏按钮的行为钩子。

Parameters

Collapse all

    要插入的媒体节点类型。

Returnsobject

Collapse all

    插入媒体节点并使编辑器获得焦点的回调函数。

useFloatingMediaEditButton

处理浮动媒体编辑按钮。

Returnsobject

Collapse all

    处理按钮点击的回调函数。

useFloatingMediaUrlInput

处理媒体元素的 URL 输入字段。

Props

Collapse all

    URL 输入字段的默认值。

Returnsobject

Collapse all

    处理输入变化的回调函数。

    URL 输入字段是否应在挂载时获得焦点。

    URL 输入字段的默认值。

useImage

图片元素的钩子。

Returnsobject

Collapse all

    媒体元素的 URL。

    图片的说明文字。

    图片是否可拖动。

工具函数

parseMediaUrl

解析媒体 URL 以进行插件特定的处理。

Parameters

Collapse all

    媒体插件的键。

    要解析的媒体 URL。

parseVideoUrl

解析视频 URL 并提取视频 ID 和提供商特定的嵌入 URL。

Parameters

Collapse all

    要解析的视频 URL。

ReturnsEmbedUrlData | undefined

    如果解析成功,返回包含视频 ID 和提供商的对象;如果 URL 无效或不支持,则返回 undefined。

parseTwitterUrl

解析 Twitter URL 并提取推文 ID。

Parameters

Collapse all

    Twitter URL。

Returns

Collapse all

    如果解析成功,返回包含推文 ID 和提供商的对象。 如果 URL 无效或不匹配任何支持的视频提供商,则返回 undefined。

parseIframeUrl

解析 iframe 嵌入的 URL。

Parameters

Collapse all

    iframe 的 URL 或嵌入代码。

isImageUrl

检查 URL 是否为有效的图片 URL。

Parameters

Collapse all

    要检查的 URL。

Returnsboolean

    URL 是否为有效的图片 URL。

submitFloatingMedia

提交浮动媒体元素。

Parameters

Collapse all

    要提交的浮动媒体元素。

    媒体插件的键。

withImageUpload

为编辑器实例添加图片上传功能。

Parameters

Collapse all

    Plate 插件。

withImageEmbed

为编辑器实例添加图片相关功能。

Parameters

Collapse all

    Plate 插件。

类型

TMediaElement

export interface TMediaElement extends TElement {
  url: string;
  id?: string;
  align?: 'center' | 'left' | 'right';
  isUpload?: boolean;
  name?: string;
  placeholderId?: string;
}

TPlaceholderElement

export interface TPlaceholderElement extends TElement {
  mediaType: string;
}

EmbedUrlData

export interface EmbedUrlData {
  url?: string;
  provider?: string;
  id?: string;
  component?: React.FC<EmbedUrlData>;
}