snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:11:02 +05:00
commit 083a46e93b
2115 changed files with 245958 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
# @tiptap/extension-file-handler
[![Version](https://img.shields.io/npm/v/@tiptap/extension-file-handler.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-file-handler)
[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-file-handler.svg)](https://npmcharts.com/compare/tiptap?minimal=true)
[![License](https://img.shields.io/npm/l/@tiptap/extension-file-handler.svg)](https://www.npmjs.com/package/@tiptap/extension-file-handler)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis)
## Introduction
Tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as _New York Times_, _The Guardian_ or _Atlassian_.
## Official Documentation
Documentation can be found on the [Tiptap website](https://tiptap.dev).
## License
Tiptap is open sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap/blob/main/LICENSE.md).

View File

@@ -0,0 +1,54 @@
{
"name": "@tiptap/extension-file-handler",
"version": "3.30.3",
"description": "file handler extension for tiptap",
"keywords": [
"tiptap",
"tiptap extension"
],
"homepage": "https://tiptap.dev/docs/editor/extensions/functionality/filehandler",
"bugs": {
"url": "https://github.com/ueberdosis/tiptap/issues"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ueberdosis/tiptap",
"directory": "packages/extension-file-handler"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"files": [
"src",
"dist"
],
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": {
"import": "./dist/index.d.ts",
"require": "./dist/index.d.cts"
},
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"scripts": {
"build": "vp pack"
},
"devDependencies": {
"@tiptap/core": "workspace:^",
"@tiptap/extension-text-style": "workspace:^",
"@tiptap/pm": "workspace:^"
},
"peerDependencies": {
"@tiptap/core": "workspace:*",
"@tiptap/extension-text-style": "workspace:*",
"@tiptap/pm": "workspace:*"
}
}

View File

@@ -0,0 +1,92 @@
import { Plugin, PluginKey } from '@tiptap/pm/state'
import type { FileHandlePluginOptions } from './types.js'
export const FileHandlePlugin = ({
key,
editor,
onPaste,
onDrop,
allowedMimeTypes,
consumePasteEvent,
}: FileHandlePluginOptions) => {
return new Plugin({
key: key || new PluginKey('fileHandler'),
props: {
handleDrop(_view, event) {
if (!onDrop) {
return false
}
// If the drop contains ProseMirror internal slice data, let ProseMirror
// handle it — this is an internal content drag (e.g. moving an image),
// not an external file drop
if (event.dataTransfer?.types.includes('application/x-prosemirror-slice')) {
return false
}
if (!event.dataTransfer?.files.length) {
return false
}
const dropPos = _view.posAtCoords({
left: event.clientX,
top: event.clientY,
})
let filesArray = Array.from(event.dataTransfer.files)
if (allowedMimeTypes) {
filesArray = filesArray.filter(file => allowedMimeTypes.includes(file.type))
}
if (filesArray.length === 0) {
return false
}
event.preventDefault()
event.stopPropagation()
onDrop(editor, filesArray, dropPos?.pos || 0)
return true
},
handlePaste(_view, event) {
if (!onPaste) {
return false
}
if (!event.clipboardData?.files.length) {
return false
}
let filesArray = Array.from(event.clipboardData.files)
const htmlContent = event.clipboardData.getData('text/html')
if (allowedMimeTypes) {
filesArray = filesArray.filter(file => allowedMimeTypes.includes(file.type))
}
if (filesArray.length === 0) {
return false
}
event.preventDefault()
event.stopPropagation()
onPaste(editor, filesArray, htmlContent)
// When HTML content is present alongside files (e.g. animated GIFs
// copied as PNG), and consumePasteEvent is not set, let other
// extensions handle the HTML content via their parseHTML/paste rules.
if (htmlContent.length > 0 && !consumePasteEvent) {
return false
}
return true
},
},
})
}

View File

@@ -0,0 +1,31 @@
import { Extension } from '@tiptap/core'
import { PluginKey } from '@tiptap/pm/state'
import { FileHandlePlugin } from './FileHandlePlugin.js'
import type { FileHandlerOptions } from './types.js'
export const FileHandler = Extension.create<FileHandlerOptions>({
name: 'fileHandler',
addOptions() {
return {
onPaste: undefined,
onDrop: undefined,
allowedMimeTypes: undefined,
consumePasteEvent: false,
}
},
addProseMirrorPlugins() {
return [
FileHandlePlugin({
key: new PluginKey(this.name),
editor: this.editor,
allowedMimeTypes: this.options.allowedMimeTypes,
onDrop: this.options.onDrop,
onPaste: this.options.onPaste,
consumePasteEvent: this.options.consumePasteEvent,
}),
]
},
})

View File

@@ -0,0 +1,8 @@
import { FileHandler } from './fileHandler.js'
export * from './FileHandlePlugin.js'
export * from './types.js'
export { FileHandler }
export default FileHandler

View File

@@ -0,0 +1,54 @@
import type { Editor } from '@tiptap/core'
import type { PluginKey } from '@tiptap/pm/state'
export type FileHandlePluginOptions = {
/**
* The plugin key.
* @default 'fileHandler'
*/
key?: PluginKey
/**
* The editor instance.
*/
editor: Editor
/**
* An array of allowed mimetypes
*
* example: ['image/jpeg', 'image/png']
*/
allowedMimeTypes?: string[]
/**
* The onPaste callback that is called when a file is pasted.
* @param editor the editor instance
* @param files the File array including File objects
* @param pasteContent the pasted content as HTML string - this is only available if there is also html copied to the clipboard for example by copying from a website
* @returns Returns nothing.
*/
onPaste?: (editor: Editor, files: File[], pasteContent?: string) => void
/**
* When true, the paste event is fully consumed when files are present,
* even if HTML content exists in the clipboard. This prevents paste
* rules from other extensions from running on the same paste event
* and creating duplicate content.
*
* Set this to true when your onPaste callback handles file insertion
* and you want to prevent other extensions from processing the same paste.
*
* @default false
*/
consumePasteEvent?: boolean
/**
* The onDrop callback that is called when a file is dropped.
* @param editor the editor instance
* @param files the File array including File objects
* @returns Returns nothing.
*/
onDrop?: (editor: Editor, files: File[], pos: number) => void
}
export type FileHandlerOptions = {} & Omit<FileHandlePluginOptions, 'key' | 'editor'>

View File

@@ -0,0 +1,403 @@
import { Editor } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import { FileHandler } from '@tiptap/extension-file-handler'
import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
const createMockEvent = (files: File[], htmlContent?: string) => {
return {
clipboardData: {
files,
getData(type: string) {
if (type === 'text/html') {
return htmlContent || ''
}
return ''
},
},
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
} as unknown as ClipboardEvent
}
const getFile = (name = 'test.png', type = 'image/png') => {
return new File(['dummy content'], name, { type })
}
describe('extension-file-handler', () => {
const editorElClass = 'tiptap'
let editor: Editor | null = null
const createEditorEl = () => {
const editorEl = document.createElement('div')
editorEl.classList.add(editorElClass)
document.body.appendChild(editorEl)
return editorEl
}
const getEditorEl = () => document.querySelector(`.${editorElClass}`)
const getHandlePaste = () => {
// Find the file handler plugin's handlePaste prop by its key name
const plugins = editor!.view.state.plugins
for (const plugin of plugins) {
if (plugin.key.includes('fileHandler') && plugin.props.handlePaste) {
return plugin.props.handlePaste
}
}
return null
}
afterEach(() => {
editor?.destroy()
getEditorEl()?.remove()
editor = null
})
describe('current behavior (regression)', () => {
it('returns false when no onPaste callback is provided', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, FileHandler],
content: '<p>test</p>',
})
const handlePaste = getHandlePaste()
const event = createMockEvent([getFile()])
const result = handlePaste!(editor!.view, event, {} as any)
expect(result).toBe(false)
})
it('returns false when clipboard has no files', () => {
const onPasteSpy = vi.fn()
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
FileHandler.configure({
onPaste: onPasteSpy,
}),
],
content: '<p>test</p>',
})
const handlePaste = getHandlePaste()
const event = createMockEvent([])
// Override clipboardData to return empty files
Object.defineProperty(event, 'clipboardData', {
value: {
files: [],
getData: () => '',
},
})
const result = handlePaste!(editor!.view, event, {} as any)
expect(result).toBe(false)
expect(onPasteSpy).not.toHaveBeenCalled()
})
it('returns false when all files are filtered out by allowedMimeTypes', () => {
const onPasteSpy = vi.fn()
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
FileHandler.configure({
onPaste: onPasteSpy,
allowedMimeTypes: ['application/pdf'],
}),
],
content: '<p>test</p>',
})
const handlePaste = getHandlePaste()
const event = createMockEvent([getFile('image.png', 'image/png')])
const result = handlePaste!(editor!.view, event, {} as any)
expect(result).toBe(false)
expect(onPasteSpy).not.toHaveBeenCalled()
})
it('calls onPaste and returns true when files are pasted without HTML', () => {
const onPasteSpy = vi.fn()
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
FileHandler.configure({
onPaste: onPasteSpy,
}),
],
content: '<p>test</p>',
})
const handlePaste = getHandlePaste()
const event = createMockEvent([getFile()])
const result = handlePaste!(editor!.view, event, {} as any)
expect(result).toBe(true)
expect(onPasteSpy).toHaveBeenCalledOnce()
expect(onPasteSpy).toHaveBeenCalledWith(editor, expect.any(Array), '')
})
it('calls onPaste and returns false when files are pasted with HTML', () => {
const onPasteSpy = vi.fn()
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
FileHandler.configure({
onPaste: onPasteSpy,
}),
],
content: '<p>test</p>',
})
const handlePaste = getHandlePaste()
const event = createMockEvent([getFile()], '<img src="test.png">')
const result = handlePaste!(editor!.view, event, {} as any)
// When HTML is present and consumePasteEvent is false, returns false
expect(result).toBe(false)
expect(onPasteSpy).toHaveBeenCalledOnce()
})
it('receives htmlContent in onPaste callback when HTML is present', () => {
const onPasteSpy = vi.fn()
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
FileHandler.configure({
onPaste: onPasteSpy,
}),
],
content: '<p>test</p>',
})
const handlePaste = getHandlePaste()
const htmlContent = '<img src="test.png">'
const event = createMockEvent([getFile()], htmlContent)
handlePaste!(editor!.view, event, {} as any)
expect(onPasteSpy).toHaveBeenCalledWith(editor, expect.any(Array), htmlContent)
})
})
describe('consumePasteEvent option', () => {
it('defaults to false (backward compatible)', () => {
const onPasteSpy = vi.fn()
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
FileHandler.configure({
onPaste: onPasteSpy,
}),
],
content: '<p>test</p>',
})
const handlePaste = getHandlePaste()
const event = createMockEvent([getFile()], '<img src="test.png">')
const result = handlePaste!(editor!.view, event, {} as any)
// Default: onPaste is called but event is not consumed (returns false)
expect(result).toBe(false)
expect(onPasteSpy).toHaveBeenCalledOnce()
})
it('returns true when files are pasted with HTML and consumePasteEvent is true', () => {
const onPasteSpy = vi.fn()
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
FileHandler.configure({
onPaste: onPasteSpy,
consumePasteEvent: true,
}),
],
content: '<p>test</p>',
})
const handlePaste = getHandlePaste()
const event = createMockEvent([getFile()], '<img src="test.png">')
const result = handlePaste!(editor!.view, event, {} as any)
// With consumePasteEvent: true, returns true even with HTML
expect(result).toBe(true)
expect(onPasteSpy).toHaveBeenCalledOnce()
})
it('returns true when files are pasted without HTML and consumePasteEvent is true', () => {
const onPasteSpy = vi.fn()
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
FileHandler.configure({
onPaste: onPasteSpy,
consumePasteEvent: true,
}),
],
content: '<p>test</p>',
})
const handlePaste = getHandlePaste()
const event = createMockEvent([getFile()])
const result = handlePaste!(editor!.view, event, {} as any)
expect(result).toBe(true)
expect(onPasteSpy).toHaveBeenCalledOnce()
})
it('still receives htmlContent in onPaste callback when consumePasteEvent is true', () => {
const onPasteSpy = vi.fn()
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
FileHandler.configure({
onPaste: onPasteSpy,
consumePasteEvent: true,
}),
],
content: '<p>test</p>',
})
const handlePaste = getHandlePaste()
const htmlContent = '<img src="test.png">'
const event = createMockEvent([getFile()], htmlContent)
handlePaste!(editor!.view, event, {} as any)
expect(onPasteSpy).toHaveBeenCalledWith(editor, expect.any(Array), htmlContent)
})
it('receives correct files array in onPaste callback when consumePasteEvent is true', () => {
let receivedFiles: File[] = []
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
FileHandler.configure({
onPaste: (_editor, files) => {
receivedFiles = files
},
consumePasteEvent: true,
}),
],
content: '<p>test</p>',
})
const handlePaste = getHandlePaste()
const file1 = getFile('image1.png', 'image/png')
const file2 = getFile('image2.png', 'image/png')
const event = createMockEvent([file1, file2], '<img src="test.png">')
handlePaste!(editor!.view, event, {} as any)
// Check files array has correct length and types
expect(receivedFiles).toHaveLength(2)
expect(receivedFiles[0].type).toBe('image/png')
expect(receivedFiles[1].type).toBe('image/png')
})
it('calls preventDefault and stopPropagation when files are present', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
FileHandler.configure({
onPaste: vi.fn(),
consumePasteEvent: true,
}),
],
content: '<p>test</p>',
})
const handlePaste = getHandlePaste()
const event = createMockEvent([getFile()])
handlePaste!(editor!.view, event, {} as any)
expect(event.preventDefault).toHaveBeenCalledOnce()
expect(event.stopPropagation).toHaveBeenCalledOnce()
})
it('does not call onPaste when files are filtered out by allowedMimeTypes', () => {
const onPasteSpy = vi.fn()
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
FileHandler.configure({
onPaste: onPasteSpy,
allowedMimeTypes: ['application/pdf'],
consumePasteEvent: true,
}),
],
content: '<p>test</p>',
})
const handlePaste = getHandlePaste()
const event = createMockEvent([getFile('image.png', 'image/png')])
const result = handlePaste!(editor!.view, event, {} as any)
expect(result).toBe(false)
expect(onPasteSpy).not.toHaveBeenCalled()
})
})
})

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vite-plus'
import { basePackConfig } from '../../pack.config.mjs'
export default defineConfig({
pack: {
entry: ['src/index.ts'],
...basePackConfig(),
},
})