npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tiptap-codeless/extension-file-upload

v1.0.0

Published

Tiptap file upload extension with drag and drop, paste, image resize, preview cards, pluggable storage, and built-in i18n

Readme

@tiptap-codeless/extension-file-upload

Tiptap 文件上传扩展:支持拖拽、粘贴、预览、缩放和可插拔存储。


✨ 特性

  • 🎯 拖拽上传 – 支持将文件拖拽到编辑器中上传
  • 📋 粘贴上传 – 支持从剪贴板粘贴图片/文件
  • 🖼️ 图片预览与缩放 – 图片即时预览,选中后可通过四角拖拽缩放
  • 📐 图片对齐 – 支持左对齐、居中、右对齐(带气泡菜单)
  • 🎬 视频预览 – 视频文件可直接在编辑器中预览播放
  • 📁 文件卡片 – 非图片/视频文件以卡片形式展示,包含文件信息
  • 💾 多种存储模式 – 支持内存、Base64、本地文件系统、自定义上传
  • 上传占位区 – 拖拽/粘贴后立即展示临时占位区,并支持可选上传进度
  • ⚙️ 文件类型与大小控制 – 可配置接受的文件类型和大小限制

📦 安装

pnpm add @tiptap-codeless/extension-file-upload

本包是 ESM-only,面向现代 React + 打包工具。

样式会在扩展初始化时 自动注入,无需单独引入 CSS。


🚀 基本用法

import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { FileUpload } from '@tiptap-codeless/extension-file-upload';

const editor = useEditor({
    extensions: [
        StarterKit,
        FileUpload.configure({
            storage: {
                mode: 'memory', // 默认值
            },
        }),
    ],
});

function App() {
    return <EditorContent editor={editor} />;
}

💾 存储模式

1. Memory 模式(默认)

使用 Object URL,文件仅在内存中,刷新页面后丢失。

FileUpload.configure({
    storage: {
        mode: 'memory',
    },
});

2. Base64 模式

将文件转换为 Base64 Data URL,可以持久化到文档中(但会增大文档体积)。

FileUpload.configure({
    storage: {
        mode: 'base64',
    },
});

3. Local 模式

使用 File System Access API 将文件保存到用户选择的本地目录。

FileUpload.configure({
    storage: {
        mode: 'local',
        // 可选:提供已有的目录句柄,避免重复弹窗选择
        directoryHandle: undefined,
        // 可选:自定义文件名生成器
        fileName: (file) => `${Date.now()}-${file.name}`,
        // 可选:是否在每次上传时都请求新的目录(默认 false)
        alwaysAskDirectory: false,
    },
});

4. Custom 模式

使用自定义上传处理器,适用于上传到 OSS、云存储等场景。 扩展负责编辑器内的上传占位和最终节点替换,业务代码只需要完成上传并返回最终资源信息。

FileUpload.configure({
    storage: {
        mode: 'custom',
        upload: async (files, ctx) => {
            // 上传到你的服务器或云存储
            const results = await Promise.all(
                files.map(async (file) => {
                    const formData = new FormData();
                    formData.append('file', file);
                    const response = await fetch('/api/upload', {
                        method: 'POST',
                        body: formData,
                    });
                    const { url } = await response.json();
                    return {
                        kind: getFileKind(file), // 'image' | 'video' | 'file'
                        url,
                        name: file.name,
                        mimeType: file.type,
                        size: file.size,
                    };
                })
            );
            return { assets: results };
        },
    },
});

如果你的上传 SDK 能拿到进度,可以调用 ctx.onProgress。如果拿不到真实进度,占位区会保持不确定加载状态。

FileUpload.configure({
    storage: {
        mode: 'custom',
        upload: async (files, ctx) => {
            const assets = await Promise.all(
                files.map((file) =>
                    uploadToCos(file, {
                        onProgress: ({ loaded, total }) => {
                            ctx.onProgress?.({ file, loaded, total });
                        },
                    })
                )
            );

            return { assets };
        },
    },
});

⚙️ 配置选项

| 选项 | 类型 | 默认值 | 描述 | | --------------------- | --------------------------------------------- | --------------------------------- | -------------------------------------- | | storage.mode | 'memory' \| 'base64' \| 'local' \| 'custom' | 'memory' | 存储模式 | | storage.upload | UploadHandler | undefined | 自定义上传处理器(仅 custom 模式必填) | | storage.directoryHandle | FileSystemDirectoryHandle | undefined | local 模式复用已有目录句柄 | | storage.fileName | (file: File) => string | undefined | 自定义文件名策略 | | storage.alwaysAskDirectory | boolean | false | local 模式下每次都重新选择目录 | | picker.accept | string | undefined | 文件选择器接受的文件类型 | | picker.multiple | boolean | true | 是否允许多选 | | ingest.paste | boolean | true | 是否处理粘贴事件 | | ingest.drop | boolean | true | 是否处理拖放事件 | | ingest.allowedMimeTypes | string[] | undefined | paste/drop 允许的 MIME 类型 | | ingest.maxFileSize | number | undefined | 单文件最大大小(字节) | | ui.bubbleMenu | { enabled?: boolean; zIndex?: number } | { enabled: true, zIndex: 1000 } | 图片气泡菜单配置 | | ui.uploadPlaceholder | { enabled?: boolean } | { enabled: true } | 上传临时占位区配置 | | onError | (error: unknown) => void | undefined | 错误回调 |


⏳ 上传占位区与进度

FileUpload 使用 ProseMirror decoration 展示上传反馈,不会把占位区写入文档 JSON。

  • 开始上传:在插入位置立即展示占位区。
  • 上传成功:移除占位区,并替换为最终的图片、视频或文件卡片节点。
  • 上传失败:移除占位区,并通过 onError 把错误交给业务侧提示。
  • 上传进度是可选能力:可以通过 ctx.onProgress 上报 { percent }{ loaded, total }

上传传输仍然属于业务侧。COS/OSS/S3 的签名、token、bucket、最终 URL 等信息应该留在应用或服务端代码里。


🔒 只读模式

FileUpload 跟随 Tiptap 编辑器级别的只读状态,不需要在扩展里额外配置 readonly 选项。

const editor = useEditor({
    editable: false,
    extensions: [StarterKit, FileUpload],
});

// 运行时切换
editor?.setEditable(false);
editor?.setEditable(true);

编辑器处于只读状态时:

  • 上传入口会禁用:openFileDialoginsertFiles、粘贴上传、拖拽上传都会 no-op / 返回 false
  • 图片修改 UI 会隐藏:缩放手柄和对齐气泡菜单不会展示。
  • 已上传的图片、视频、文件卡片仍可用于查看、播放或下载,但拖拽 / 移动等修改行为会禁用。

💻 可用命令

注册扩展后,你可以使用以下命令:

// 打开文件选择器
editor.commands.openFileDialog({ accept: 'image/*', multiple: true });

// 直接插入文件
editor.commands.insertFiles({ files: [...] });

📦 工具函数

import {
    createObjectUrlUpload,
    createBase64Upload,
    createLocalStorageUpload,
    createUploadHandler,
    clearCachedDirectoryHandle,
} from '@tiptap-codeless/extension-file-upload';

// 清除缓存的目录句柄(用于 local 模式)
clearCachedDirectoryHandle();

📖 示例

图片缩放和对齐

选中图片后,可以通过四角拖拽调整图片大小,并通过气泡菜单设置对齐方式(左对齐、居中、右对齐)。

视频预览

上传的视频文件会在编辑器中显示为可播放的视频播放器。

文件卡片

非图片/视频文件(如 PDF、Word 等)会以卡片形式展示,包含文件名、文件类型、文件大小等信息。

你可以在仓库中的 examples/file-upload 目录查看完整示例(包含 React + Tiptap 集成)。


📄 许可证

MIT © namelesserlx