mirror of
https://github.com/transloadit/uppy.git
synced 2026-07-17 16:50:22 +00:00
Add useDropzone & useFileInput (#5735)
Co-authored-by: Mikael Finstad <finstaden@gmail.com>
This commit is contained in:
parent
52870a1911
commit
5c0c831937
51 changed files with 2892 additions and 269 deletions
|
|
@ -10,23 +10,28 @@ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|||
const rootDir = path.resolve(scriptDir, '..')
|
||||
|
||||
// Define paths
|
||||
// source:
|
||||
const COMPONENTS_DIR = path.join(rootDir, 'packages/@uppy/components/src')
|
||||
const REACT_DIR = path.join(rootDir, 'packages/@uppy/react/src/headless')
|
||||
const VUE_DIR = path.join(rootDir, 'packages/@uppy/vue/src/headless')
|
||||
// destinations:
|
||||
const REACT_DIR = path.join(rootDir, 'packages/@uppy/react/src/headless/generated')
|
||||
const VUE_DIR = path.join(rootDir, 'packages/@uppy/vue/src/headless/generated')
|
||||
const SVELTE_DIR = path.join(
|
||||
rootDir,
|
||||
'packages/@uppy/svelte/src/lib/components/headless',
|
||||
'packages/@uppy/svelte/src/lib/components/headless/generated',
|
||||
)
|
||||
|
||||
// Templates
|
||||
const REACT_TEMPLATE = `import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
const REACT_TEMPLATE = `\
|
||||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
%%ComponentName%% as %%PreactComponentName%%,
|
||||
type %%PropsTypeName%%,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
import { UppyContext } from '../UppyContextProvider.js'
|
||||
|
||||
export default function %%ComponentName%%(props: Omit<%%PropsTypeName%%, 'ctx'>) {
|
||||
const ref = useRef(null)
|
||||
|
|
@ -48,7 +53,10 @@ export default function %%ComponentName%%(props: Omit<%%PropsTypeName%%, 'ctx'>)
|
|||
}
|
||||
`
|
||||
|
||||
const VUE_TEMPLATE = `import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
const VUE_TEMPLATE = `\
|
||||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
import {
|
||||
%%ComponentName%% as %%PreactComponentName%%,
|
||||
type %%PropsTypeName%%,
|
||||
|
|
@ -56,7 +64,7 @@ import {
|
|||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from './useUppyContext.js'
|
||||
import { useUppyContext } from '../useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<%%PropsTypeName%%, 'ctx'>>({
|
||||
name: '%%ComponentName%%',
|
||||
|
|
@ -98,16 +106,19 @@ export default defineComponent<Omit<%%PropsTypeName%%, 'ctx'>>({
|
|||
})
|
||||
`
|
||||
|
||||
const SVELTE_TEMPLATE = `<script lang="ts">
|
||||
const SVELTE_TEMPLATE = `\
|
||||
<script lang="ts">
|
||||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { getContext, mount } from 'svelte'
|
||||
import {
|
||||
%%ComponentName%% as %%PreactComponentName%%,
|
||||
type %%PropsTypeName%%,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from '../UppyContextProvider.svelte'
|
||||
|
||||
const props: Omit<%%PropsTypeName%%, 'ctx'> = $props()
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
|
|
@ -211,7 +222,7 @@ try {
|
|||
const reactIndexContent = reactComponents
|
||||
.map((name) => `export { default as ${name} } from './${name}.js'`)
|
||||
.join('\n')
|
||||
await fs.writeFile(path.join(REACT_DIR, 'index.ts'), reactIndexContent)
|
||||
await fs.writeFile(path.join(REACT_DIR, 'index.ts'), `${reactIndexContent}\n`)
|
||||
console.log(`\nExporting React components from ${REACT_DIR}`)
|
||||
}
|
||||
|
||||
|
|
@ -219,7 +230,7 @@ try {
|
|||
const vueIndexContent = vueComponents
|
||||
.map((name) => `export { default as ${name} } from './${name}.js'`)
|
||||
.join('\n')
|
||||
await fs.writeFile(path.join(VUE_DIR, 'index.ts'), vueIndexContent)
|
||||
await fs.writeFile(path.join(VUE_DIR, 'index.ts'), `${vueIndexContent}\n`)
|
||||
console.log(`Exporting Vue components from ${VUE_DIR}`)
|
||||
}
|
||||
|
||||
|
|
@ -227,38 +238,10 @@ try {
|
|||
const svelteIndexContent = svelteComponents
|
||||
.map((name) => `export { default as ${name} } from './${name}.svelte'`)
|
||||
.join('\n')
|
||||
await fs.writeFile(path.join(SVELTE_DIR, 'index.ts'), svelteIndexContent)
|
||||
await fs.writeFile(path.join(SVELTE_DIR, 'index.ts'), `${svelteIndexContent}\n`)
|
||||
console.log(`Exporting Svelte components from ${SVELTE_DIR}`)
|
||||
}
|
||||
|
||||
// Copy CSS file
|
||||
const CSS_SOURCE_PATH = path.join(
|
||||
rootDir,
|
||||
'packages/@uppy/components/dist/styles.css',
|
||||
)
|
||||
const FRAMEWORK_DIRS_FOR_CSS = [
|
||||
path.join(rootDir, 'packages/@uppy/react/dist'),
|
||||
path.join(rootDir, 'packages/@uppy/vue/dist'),
|
||||
path.join(rootDir, 'packages/@uppy/svelte/dist'),
|
||||
]
|
||||
|
||||
try {
|
||||
await fs.access(CSS_SOURCE_PATH) // Check if source CSS exists
|
||||
await Promise.all(
|
||||
FRAMEWORK_DIRS_FOR_CSS.map(async (destDir) => {
|
||||
if (!existsSync(destDir)) {
|
||||
await fs.mkdir(destDir, { recursive: true })
|
||||
}
|
||||
const destPath = path.join(destDir, 'styles.css')
|
||||
await fs.copyFile(CSS_SOURCE_PATH, destPath)
|
||||
}),
|
||||
)
|
||||
} catch (cssError) {
|
||||
console.error(
|
||||
`${CSS_SOURCE_PATH} does not exist yet. Run \`yarn build\` first.`,
|
||||
)
|
||||
}
|
||||
|
||||
console.log('\nAll wrappers and index files generated successfully!')
|
||||
} catch (error) {
|
||||
console.error('Error generating wrappers:', error)
|
||||
|
|
|
|||
32
bin/copy-tailwind-css.mjs
Normal file
32
bin/copy-tailwind-css.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// todo merge this with build-css.js?
|
||||
|
||||
// Get the directory of this script
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const rootDir = path.resolve(scriptDir, '..')
|
||||
|
||||
// Copy CSS file
|
||||
const CSS_SOURCE_PATH = path.join(
|
||||
rootDir,
|
||||
'packages/@uppy/components/dist/styles.css',
|
||||
)
|
||||
const FRAMEWORK_DIRS_FOR_CSS = [
|
||||
'packages/@uppy/react/dist',
|
||||
'packages/@uppy/vue/dist',
|
||||
'packages/@uppy/svelte/dist',
|
||||
]
|
||||
|
||||
await fs.access(CSS_SOURCE_PATH) // Check if source CSS exists
|
||||
await Promise.all(
|
||||
FRAMEWORK_DIRS_FOR_CSS.map(async (destDir) => {
|
||||
const destDirPath = path.join(rootDir, destDir)
|
||||
await fs.mkdir(destDirPath, { recursive: true })
|
||||
const destPath = path.join(destDirPath, 'styles.css')
|
||||
await fs.copyFile(CSS_SOURCE_PATH, destPath)
|
||||
}),
|
||||
)
|
||||
|
|
@ -9,10 +9,10 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.0.9",
|
||||
"@uppy/core": "^4.4.2",
|
||||
"@uppy/react": "workspace:^",
|
||||
"@uppy/tus": "^4.2.2",
|
||||
"@uppy/webcam": "^4.1.1",
|
||||
"@uppy/core": "workspace:*",
|
||||
"@uppy/react": "workspace:*",
|
||||
"@uppy/tus": "workspace:*",
|
||||
"@uppy/webcam": "workspace:*",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwindcss": "^4.0.9"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
/* eslint-disable react/button-has-type */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable react/react-in-jsx-scope */
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Dropzone,
|
||||
FilesGrid,
|
||||
FilesList,
|
||||
ProviderIcon,
|
||||
UploadButton,
|
||||
UppyContextProvider,
|
||||
useDropzone,
|
||||
useFileInput,
|
||||
} from '@uppy/react'
|
||||
import Uppy from '@uppy/core'
|
||||
import Tus from '@uppy/tus'
|
||||
|
|
@ -13,6 +18,37 @@ import Tus from '@uppy/tus'
|
|||
import './app.css'
|
||||
import '@uppy/react/dist/styles.css'
|
||||
|
||||
function CustomDropzone() {
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
noClick: true,
|
||||
})
|
||||
const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input {...getInputProps()} className="hidden" />
|
||||
<div
|
||||
{...getRootProps()}
|
||||
role="button"
|
||||
className="border-2 border-dashed border-gray-300 rounded-lg p-6 bg-gray-50 transition-colors duration-200"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center h-full space-y-3">
|
||||
<input {...getFileInputProps()} className="hidden" />
|
||||
<button
|
||||
{...getButtonProps()}
|
||||
className="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
<div className="bg-white shadow-md rounded-md p-1">
|
||||
<ProviderIcon provider="device" fill="#02B383" />
|
||||
</div>
|
||||
Device
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [uppy] = useState(() =>
|
||||
new Uppy().use(Tus, {
|
||||
|
|
@ -23,21 +59,25 @@ function App() {
|
|||
return (
|
||||
<UppyContextProvider uppy={uppy}>
|
||||
<main className="p-5 max-w-xl mx-auto">
|
||||
<h1 className="text-4xl font-bold">Welcome to React.</h1>
|
||||
<h1 className="text-4xl font-bold my-4">Welcome to React.</h1>
|
||||
|
||||
<UploadButton />
|
||||
|
||||
<article>
|
||||
<h2 className="text-2xl my-4">With list</h2>
|
||||
<Dropzone />
|
||||
|
||||
<FilesList />
|
||||
<UploadButton />
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2 className="text-2xl my-4">With grid</h2>
|
||||
<Dropzone />
|
||||
<FilesGrid columns={2} />
|
||||
<UploadButton />
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2 className="text-2xl my-4">With custom dropzone</h2>
|
||||
<CustomDropzone />
|
||||
</article>
|
||||
</main>
|
||||
</UppyContextProvider>
|
||||
|
|
|
|||
28
examples/sveltekit/src/components/CustomDropzone.svelte
Normal file
28
examples/sveltekit/src/components/CustomDropzone.svelte
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts">
|
||||
import { useDropzone, useFileInput, ProviderIcon } from '@uppy/svelte'
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({ noClick: true })
|
||||
const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<input {...getInputProps()} class="uppy:hidden" />
|
||||
<div
|
||||
{...getRootProps()}
|
||||
role="button"
|
||||
class="border-2 border-dashed border-gray-300 rounded-lg p-6 bg-gray-50 transition-colors duration-200"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center h-full space-y-3">
|
||||
<input {...getFileInputProps()} class="hidden" />
|
||||
<button
|
||||
{...getButtonProps()}
|
||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
<div class="bg-white shadow-md rounded-md p-1">
|
||||
<ProviderIcon provider="device" fill="#02B383" />
|
||||
</div>
|
||||
Device
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -10,6 +10,8 @@
|
|||
} from '@uppy/svelte'
|
||||
import '@uppy/svelte/dist/styles.css'
|
||||
|
||||
import CustomDropzone from '../components/CustomDropzone.svelte'
|
||||
|
||||
const uppy = new Uppy().use(Tus, {
|
||||
endpoint: 'https://tusd.tusdemo.net/files/',
|
||||
})
|
||||
|
|
@ -17,20 +19,25 @@
|
|||
|
||||
<UppyContextProvider {uppy}>
|
||||
<main class="p-5 max-w-xl mx-auto">
|
||||
<h1 class="text-4xl font-bold">Welcome to SvelteKit.</h1>
|
||||
<h1 class="text-4xl font-bold my-4">Welcome to SvelteKit.</h1>
|
||||
|
||||
<UploadButton />
|
||||
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With list</h2>
|
||||
<Dropzone />
|
||||
<FilesList />
|
||||
<UploadButton />
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With grid</h2>
|
||||
<Dropzone />
|
||||
<FilesGrid columns={2} />
|
||||
<UploadButton />
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With custom dropzone</h2>
|
||||
<CustomDropzone />
|
||||
</article>
|
||||
</main>
|
||||
</UppyContextProvider>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,25 @@
|
|||
<template>
|
||||
<UppyContextProvider :uppy="uppy">
|
||||
<main class="p-5 max-w-xl mx-auto">
|
||||
<h1 class="text-4xl font-bold">Welcome to Vue.</h1>
|
||||
<h1 class="text-4xl font-bold my-4">Welcome to Vue.</h1>
|
||||
|
||||
<UploadButton />
|
||||
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With list</h2>
|
||||
<Dropzone />
|
||||
<FilesList />
|
||||
<UploadButton />
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With grid</h2>
|
||||
<Dropzone />
|
||||
<FilesGrid :columns="2" />
|
||||
<UploadButton />
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With custom dropzone</h2>
|
||||
<CustomDropzone />
|
||||
</article>
|
||||
</main>
|
||||
</UppyContextProvider>
|
||||
|
|
@ -31,6 +36,7 @@ import {
|
|||
FilesGrid,
|
||||
UploadButton,
|
||||
} from '@uppy/vue'
|
||||
import CustomDropzone from './Dropzone.vue'
|
||||
|
||||
const uppy = computed(() =>
|
||||
new Uppy().use(Tus, {
|
||||
|
|
|
|||
28
examples/vue3/src/Dropzone.vue
Normal file
28
examples/vue3/src/Dropzone.vue
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<input v-bind="getInputProps()" class="hidden" />
|
||||
<div
|
||||
v-bind="getRootProps()"
|
||||
class="border-2 border-dashed border-gray-300 rounded-lg p-6 bg-gray-50 transition-colors duration-200"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center h-full space-y-3">
|
||||
<input v-bind="getFileInputProps()" class="hidden" />
|
||||
<button
|
||||
v-bind="getButtonProps()"
|
||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
<div class="bg-white shadow-md rounded-md p-1">
|
||||
<ProviderIcon provider="device" fill="#02B383" />
|
||||
</div>
|
||||
Device
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDropzone, useFileInput, ProviderIcon } from '@uppy/vue'
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
noClick: true,
|
||||
})
|
||||
const { getInputProps: getFileInputProps, getButtonProps } = useFileInput()
|
||||
</script>
|
||||
12
package.json
12
package.json
|
|
@ -115,13 +115,13 @@
|
|||
"build:bundle": "yarn node ./bin/build-bundle.mjs",
|
||||
"build:clean": "cp .gitignore .gitignore.bak && printf '!node_modules\n!**/node_modules/**/*\n' >> .gitignore; git clean -Xfd packages e2e .parcel-cache coverage; mv .gitignore.bak .gitignore",
|
||||
"build:companion": "yarn workspace @uppy/companion build",
|
||||
"build:css": "yarn node ./bin/build-css.js && yarn workspace @uppy/components build:tailwind",
|
||||
"build:css": "yarn node ./bin/build-css.js && yarn workspace @uppy/components build:tailwind && yarn node ./bin/copy-tailwind-css.mjs",
|
||||
"build:svelte": "yarn workspace @uppy/svelte build",
|
||||
"build:angular": "yarn workspace angular build",
|
||||
"build:js:typeless": "npm-run-all build:lib build:companion build:svelte",
|
||||
"build:js": "npm-run-all build:js:typeless build:locale-pack build:angular build:bundle",
|
||||
"build:ts": "yarn tsc -b tsconfig.build.json && yarn workspace @uppy/svelte build",
|
||||
"build:lib": "yarn node ./bin/build-lib.mjs",
|
||||
"build:lib": "yarn build:ts && yarn node ./bin/build-lib.mjs && yarn build:css",
|
||||
"build:locale-pack": "yarn workspace @uppy-dev/locale-pack build && eslint packages/@uppy/locales/src/en_US.ts --fix && yarn workspace @uppy-dev/locale-pack test unused",
|
||||
"build": "npm-run-all --serial build:ts --parallel build:js build:css --serial build:components size",
|
||||
"contributors:save": "yarn node ./bin/update-contributors.mjs",
|
||||
|
|
@ -157,11 +157,9 @@
|
|||
"test": "npm-run-all lint test:locale-packs:unused test:unit test:companion",
|
||||
"uploadcdn": "yarn node ./private/upload-to-cdn/index.js",
|
||||
"version": "yarn node ./bin/after-version-bump.js",
|
||||
"watch:css": "onchange 'packages/{@uppy/,}*/src/*.{scss,css}' --initial --verbose -- yarn run build:css",
|
||||
"watch:js:bundle": "onchange 'packages/{@uppy/,}*/src/**/*.{js,ts,jsx,tsx}' --initial --verbose -- yarn run build:bundle",
|
||||
"watch:js:lib": "onchange 'packages/{@uppy/,}*/src/**/*.{js,ts,jsx,tsx}' --initial --verbose -- yarn run build:lib",
|
||||
"watch:js": "npm-run-all --parallel watch:js:bundle watch:js:lib",
|
||||
"watch": "npm-run-all --parallel watch:css watch:js"
|
||||
"watch:js:bundle": "onchange 'packages/{@uppy/,}*/src/**/*.{js,ts,jsx,tsx,svelte,scss,css}' --initial --verbose -- yarn run build:bundle",
|
||||
"watch:js:lib": "onchange 'packages/{@uppy/,}*/src/**/*.{js,ts,jsx,tsx,svelte,scss,css}' --initial --verbose -- yarn run build:lib",
|
||||
"watch": "npm-run-all --parallel watch:js:bundle watch:js:lib"
|
||||
},
|
||||
"alias": {
|
||||
"preact/jsx-dev-runtime": "preact/jsx-runtime"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
"url": "git+https://github.com/transloadit/uppy.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build:tailwind": "npx @tailwindcss/cli -i ./src/input.css -o ./dist/styles.css"
|
||||
"build:tailwind": "tailwindcss -i ./src/input.css -o ./dist/styles.css"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/cli": "^4.0.6",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { h } from 'preact'
|
||||
import { useState, useRef } from 'preact/hooks'
|
||||
import { useMemo } from 'preact/hooks'
|
||||
import { clsx } from 'clsx'
|
||||
import type { UppyContext } from './types.js'
|
||||
import type { NonNullableUppyContext, UppyContext } from './types.js'
|
||||
import { createDropzone } from './hooks/dropzone.js'
|
||||
|
||||
export type DropzoneProps = {
|
||||
width?: string
|
||||
|
|
@ -13,77 +15,23 @@ export type DropzoneProps = {
|
|||
}
|
||||
|
||||
export default function Dropzone(props: DropzoneProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [isDragging, setIsDragging] = useState(() => false)
|
||||
const { width, height, note, noClick, ctx } = props
|
||||
|
||||
function handleDrop(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setIsDragging(false)
|
||||
const files = Array.from(event.dataTransfer?.files || [])
|
||||
if (!files.length) return
|
||||
ctx.uppy?.addFiles(
|
||||
files.map((file) => ({
|
||||
source: 'drag-drop',
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
data: file,
|
||||
})),
|
||||
)
|
||||
if (!ctx.uppy) {
|
||||
throw new Error('Dropzone must be used within a UppyContextProvider')
|
||||
}
|
||||
|
||||
function handleDragOver(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
function handleDragEnter(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setIsDragging(true)
|
||||
}
|
||||
|
||||
function handleDragLeave(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setIsDragging(false)
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
if (noClick) return
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
function handleFileInputChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const files = Array.from(input.files || [])
|
||||
if (!files.length) return
|
||||
ctx.uppy?.addFiles(
|
||||
files.map((file) => ({
|
||||
source: 'drag-drop',
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
data: file,
|
||||
})),
|
||||
)
|
||||
|
||||
// Reset the input value so the same file can be selected again
|
||||
input.value = ''
|
||||
}
|
||||
const { getRootProps, getInputProps } = useMemo(
|
||||
() => createDropzone(ctx as NonNullableUppyContext, { noClick }),
|
||||
[ctx, noClick],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="uppy-reset" data-uppy-element="dropzone">
|
||||
<input
|
||||
type="file"
|
||||
className="uppy:hidden"
|
||||
ref={fileInputRef}
|
||||
multiple
|
||||
onChange={handleFileInputChange}
|
||||
/>
|
||||
<input {...getInputProps()} className="uppy:hidden" />
|
||||
<div
|
||||
{...getRootProps()}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
style={{
|
||||
width: width || '100%',
|
||||
height: height || '100%',
|
||||
|
|
@ -92,18 +40,10 @@ export default function Dropzone(props: DropzoneProps) {
|
|||
'uppy:border-2 uppy:border-dashed uppy:border-gray-300',
|
||||
'uppy:rounded-lg uppy:p-6 uppy:bg-gray-50',
|
||||
'uppy:transition-colors uppy:duration-200',
|
||||
{
|
||||
'uppy:bg-blue-50': isDragging,
|
||||
},
|
||||
{
|
||||
'uppy:cursor-pointer uppy:hover:bg-blue-50': !noClick,
|
||||
},
|
||||
)}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className="uppy:flex uppy:flex-col uppy:items-center uppy:justify-center uppy:h-full uppy:space-y-3">
|
||||
<p className="uppy:text-gray-600">
|
||||
|
|
|
|||
114
packages/@uppy/components/src/hooks/dropzone.ts
Normal file
114
packages/@uppy/components/src/hooks/dropzone.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import type { NonNullableUppyContext } from '../types.js'
|
||||
|
||||
export type DropzoneOptions = {
|
||||
noClick?: boolean
|
||||
onDragOver?: (event: Event) => void
|
||||
onDragEnter?: (event: Event) => void
|
||||
onDragLeave?: (event: Event) => void
|
||||
onDrop?: (files: File[]) => void
|
||||
onFileInputChange?: (files: File[]) => void
|
||||
}
|
||||
|
||||
export type DropzoneReturn<DragEventType, ChangeEventType> = {
|
||||
getRootProps: () => {
|
||||
onDragEnter: (event: DragEventType) => void
|
||||
onDragOver: (event: DragEventType) => void
|
||||
onDragLeave: (event: DragEventType) => void
|
||||
onDrop: (event: DragEventType) => void
|
||||
onClick: () => void
|
||||
}
|
||||
getInputProps: () => {
|
||||
id: string
|
||||
type: 'file'
|
||||
multiple: boolean
|
||||
onChange: (event: ChangeEventType) => void
|
||||
}
|
||||
}
|
||||
|
||||
const fileInputId = 'uppy-dropzone-file-input' as const
|
||||
|
||||
export function createDropzone<
|
||||
DragEventType extends DragEvent,
|
||||
ChangeEventType extends Event,
|
||||
>(
|
||||
ctx: NonNullableUppyContext,
|
||||
options: DropzoneOptions = {},
|
||||
): DropzoneReturn<DragEventType, ChangeEventType> {
|
||||
const handleDrop = (event: DragEventType) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const files = Array.from(event.dataTransfer?.files ?? [])
|
||||
if (!files.length) return
|
||||
|
||||
options.onDrop?.(files)
|
||||
|
||||
ctx.uppy.addFiles(
|
||||
files.map((file) => ({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
data: file,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
const handleDragOver = (event: DragEventType) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
options.onDragOver?.(event)
|
||||
}
|
||||
|
||||
const handleDragEnter = (event: DragEventType) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
options.onDragEnter?.(event)
|
||||
}
|
||||
|
||||
const handleDragLeave = (event: DragEventType) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
options.onDragLeave?.(event)
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (options.noClick) return
|
||||
const input = document.getElementById(fileInputId) as HTMLInputElement
|
||||
input?.click()
|
||||
}
|
||||
|
||||
const handleFileInputChange = (event: ChangeEventType) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const files = Array.from(input.files ?? [])
|
||||
if (!files.length) return
|
||||
|
||||
options.onFileInputChange?.(files)
|
||||
|
||||
ctx.uppy.addFiles(
|
||||
files.map((file) => ({
|
||||
source: 'drag-drop',
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
data: file,
|
||||
})),
|
||||
)
|
||||
|
||||
// Reset the input value so the same file can be selected again
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
return {
|
||||
getRootProps: () => ({
|
||||
onDragEnter: handleDragEnter,
|
||||
onDragOver: handleDragOver,
|
||||
onDragLeave: handleDragLeave,
|
||||
onDrop: handleDrop,
|
||||
onClick: handleClick,
|
||||
}),
|
||||
getInputProps: () => ({
|
||||
id: fileInputId,
|
||||
type: 'file',
|
||||
multiple: true,
|
||||
onChange: handleFileInputChange,
|
||||
}),
|
||||
}
|
||||
}
|
||||
64
packages/@uppy/components/src/hooks/file-input.ts
Normal file
64
packages/@uppy/components/src/hooks/file-input.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import type { NonNullableUppyContext } from '../types.js'
|
||||
|
||||
export type FileInputProps = {
|
||||
multiple?: boolean
|
||||
accept?: string
|
||||
}
|
||||
|
||||
export type FileInputFunctions<EventType> = {
|
||||
getInputProps: () => {
|
||||
id: string
|
||||
type: 'file'
|
||||
multiple: boolean
|
||||
accept?: string
|
||||
onChange: (event: EventType) => void
|
||||
}
|
||||
getButtonProps: () => {
|
||||
type: 'button'
|
||||
onClick: () => void
|
||||
}
|
||||
}
|
||||
|
||||
const fileInputId = 'uppy-file-input' as const
|
||||
|
||||
// Use a more generic constraint that works with both DOM Events and React/Vue Events
|
||||
export function createFileInput<EventType extends Event>(
|
||||
ctx: NonNullableUppyContext,
|
||||
props: FileInputProps = {},
|
||||
): FileInputFunctions<EventType> {
|
||||
const handleClick = () => {
|
||||
const input = document.getElementById(fileInputId) as HTMLInputElement
|
||||
input?.click()
|
||||
}
|
||||
|
||||
const handleFileInputChange = (event: EventType) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const files = Array.from(input.files || [])
|
||||
if (!files.length) return
|
||||
|
||||
ctx.uppy.addFiles(
|
||||
files.map((file) => ({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
data: file,
|
||||
})),
|
||||
)
|
||||
|
||||
// Reset the input value so the same file can be selected again
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
return {
|
||||
getInputProps: () => ({
|
||||
id: fileInputId,
|
||||
type: 'file',
|
||||
multiple: props.multiple ?? true,
|
||||
accept: props.accept,
|
||||
onChange: handleFileInputChange,
|
||||
}),
|
||||
getButtonProps: () => ({
|
||||
type: 'button',
|
||||
onClick: handleClick,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,16 @@ export { default as Thumbnail, type ThumbnailProps } from './Thumbnail.js'
|
|||
export { default as FilesList, type FilesListProps } from './FilesList.js'
|
||||
export { default as FilesGrid, type FilesGridProps } from './FilesGrid.js'
|
||||
export { default as Dropzone, type DropzoneProps } from './Dropzone.js'
|
||||
export {
|
||||
createDropzone,
|
||||
type DropzoneReturn,
|
||||
type DropzoneOptions,
|
||||
} from './hooks/dropzone.js'
|
||||
export {
|
||||
createFileInput,
|
||||
type FileInputProps,
|
||||
type FileInputFunctions,
|
||||
} from './hooks/file-input.js'
|
||||
export {
|
||||
default as UploadButton,
|
||||
type UploadButtonProps,
|
||||
|
|
@ -11,7 +21,11 @@ export {
|
|||
type ProviderIconProps,
|
||||
} from './ProviderIcon.js'
|
||||
|
||||
export type { UppyContext } from './types.js'
|
||||
|
||||
// Types and utils
|
||||
export type {
|
||||
UppyContext,
|
||||
UppyState,
|
||||
UploadStatus,
|
||||
NonNullableUppyContext,
|
||||
} from './types.js'
|
||||
export { createUppyEventAdapter } from './uppyEventAdapter.js'
|
||||
export type { UploadStatus } from './types.js'
|
||||
|
|
|
|||
|
|
@ -8,8 +8,15 @@ export type UploadStatus =
|
|||
| 'error'
|
||||
| 'complete'
|
||||
|
||||
export type UppyContext = {
|
||||
uppy: Uppy | undefined
|
||||
export type UppyState = {
|
||||
status: UploadStatus
|
||||
progress: number
|
||||
}
|
||||
|
||||
export type UppyContext = UppyState & {
|
||||
uppy: Uppy | undefined
|
||||
}
|
||||
|
||||
export type NonNullableUppyContext = UppyContext & {
|
||||
uppy: Uppy
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Uppy from '@uppy/core'
|
||||
import Uppy, { type UppyEventMap } from '@uppy/core'
|
||||
import type { UploadStatus } from './types'
|
||||
|
||||
export function createUppyEventAdapter({
|
||||
|
|
@ -19,9 +19,13 @@ export function createUppyEventAdapter({
|
|||
const onResumeAll = () => {
|
||||
onStatusChange('uploading')
|
||||
}
|
||||
const onComplete = () => {
|
||||
onStatusChange('complete')
|
||||
onProgressChange(0)
|
||||
const onComplete: UppyEventMap<any, any>['complete'] = (result) => {
|
||||
// If there are no uploads in failed or successful 'cancel-all' was called.
|
||||
// Because 'complete' is called afterwards, we don't want to set the status to 'complete'
|
||||
if (result?.failed?.length || result?.successful?.length) {
|
||||
onStatusChange('complete')
|
||||
onProgressChange(0)
|
||||
}
|
||||
}
|
||||
const onError = () => {
|
||||
onStatusChange('error')
|
||||
|
|
|
|||
|
|
@ -3,9 +3,14 @@ import React, {
|
|||
useState,
|
||||
useEffect,
|
||||
createElement as h,
|
||||
useContext,
|
||||
} from 'react'
|
||||
import type Uppy from '@uppy/core'
|
||||
import { createUppyEventAdapter, type UploadStatus } from '@uppy/components'
|
||||
import {
|
||||
createUppyEventAdapter,
|
||||
type UploadStatus,
|
||||
type NonNullableUppyContext,
|
||||
} from '@uppy/components'
|
||||
|
||||
interface UppyContextValue {
|
||||
uppy: Uppy | undefined
|
||||
|
|
@ -62,3 +67,13 @@ export function UppyContextProvider({ uppy, children }: Props) {
|
|||
}
|
||||
|
||||
export default UppyContextProvider
|
||||
|
||||
export function useUppyContext(): NonNullableUppyContext {
|
||||
const ctx = useContext(UppyContext)
|
||||
|
||||
if (!ctx.uppy) {
|
||||
throw new Error('useDropzone must be called within a UppyContextProvider')
|
||||
}
|
||||
|
||||
return ctx as NonNullableUppyContext // covered by the if statement above
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
Dropzone as PreactDropzone,
|
||||
|
|
@ -5,7 +7,7 @@ import {
|
|||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
import { UppyContext } from '../UppyContextProvider.js'
|
||||
|
||||
export default function Dropzone(props: Omit<DropzoneProps, 'ctx'>) {
|
||||
const ref = useRef(null)
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
FilesGrid as PreactFilesGrid,
|
||||
|
|
@ -5,7 +7,7 @@ import {
|
|||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
import { UppyContext } from '../UppyContextProvider.js'
|
||||
|
||||
export default function FilesGrid(props: Omit<FilesGridProps, 'ctx'>) {
|
||||
const ref = useRef(null)
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
FilesList as PreactFilesList,
|
||||
|
|
@ -5,7 +7,7 @@ import {
|
|||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
import { UppyContext } from '../UppyContextProvider.js'
|
||||
|
||||
export default function FilesList(props: Omit<FilesListProps, 'ctx'>) {
|
||||
const ref = useRef(null)
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
ProviderIcon as PreactProviderIcon,
|
||||
|
|
@ -5,7 +7,7 @@ import {
|
|||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
import { UppyContext } from '../UppyContextProvider.js'
|
||||
|
||||
export default function ProviderIcon(props: Omit<ProviderIconProps, 'ctx'>) {
|
||||
const ref = useRef(null)
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
Thumbnail as PreactThumbnail,
|
||||
|
|
@ -5,7 +7,7 @@ import {
|
|||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
import { UppyContext } from '../UppyContextProvider.js'
|
||||
|
||||
export default function Thumbnail(props: Omit<ThumbnailProps, 'ctx'>) {
|
||||
const ref = useRef(null)
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { useEffect, useRef, useContext, createElement as h } from 'react'
|
||||
import {
|
||||
UploadButton as PreactUploadButton,
|
||||
|
|
@ -5,7 +7,7 @@ import {
|
|||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContext } from './UppyContextProvider.js'
|
||||
import { UppyContext } from '../UppyContextProvider.js'
|
||||
|
||||
export default function UploadButton(props: Omit<UploadButtonProps, 'ctx'>) {
|
||||
const ref = useRef(null)
|
||||
|
|
@ -6,10 +6,12 @@ export { default as StatusBar } from './StatusBar.js'
|
|||
export { default as FileInput } from './FileInput.js'
|
||||
export { default as useUppyState } from './useUppyState.js'
|
||||
export { default as useUppyEvent } from './useUppyEvent.js'
|
||||
export { useDropzone } from './useDropzone.js'
|
||||
export { useFileInput } from './useFileInput.js'
|
||||
|
||||
// Headless components
|
||||
export {
|
||||
UppyContext,
|
||||
UppyContextProvider,
|
||||
} from './headless/UppyContextProvider.js'
|
||||
export * from './headless/index.js'
|
||||
export * from './headless/generated/index.js'
|
||||
|
|
|
|||
37
packages/@uppy/react/src/useDropzone.ts
Normal file
37
packages/@uppy/react/src/useDropzone.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useMemo } from 'react'
|
||||
import type {
|
||||
DragEvent as ReactDragEvent,
|
||||
ChangeEvent as ReactChangeEvent,
|
||||
} from 'react'
|
||||
import {
|
||||
createDropzone,
|
||||
type DropzoneOptions,
|
||||
type DropzoneReturn,
|
||||
} from '@uppy/components'
|
||||
import { useUppyContext } from './headless/UppyContextProvider.js'
|
||||
|
||||
type TDragEvent = DragEvent & ReactDragEvent<HTMLDivElement>
|
||||
type TChangeEvent = Event & ReactChangeEvent<HTMLInputElement>
|
||||
|
||||
export function useDropzone(
|
||||
options?: DropzoneOptions,
|
||||
): DropzoneReturn<TDragEvent, TChangeEvent> {
|
||||
const ctx = useUppyContext()
|
||||
|
||||
const dropzone = useMemo(
|
||||
() => createDropzone<TDragEvent, TChangeEvent>(ctx, options),
|
||||
// We need every value on options to be memoized to avoid re-creating the dropzone on every render
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
ctx,
|
||||
options?.noClick,
|
||||
options?.onDragOver,
|
||||
options?.onDragEnter,
|
||||
options?.onDragLeave,
|
||||
options?.onDrop,
|
||||
options?.onFileInputChange,
|
||||
],
|
||||
)
|
||||
|
||||
return dropzone
|
||||
}
|
||||
21
packages/@uppy/react/src/useFileInput.ts
Normal file
21
packages/@uppy/react/src/useFileInput.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { useMemo, type ChangeEvent } from 'react'
|
||||
import type { FileInputProps, FileInputFunctions } from '@uppy/components'
|
||||
import { createFileInput } from '@uppy/components'
|
||||
import { useUppyContext } from './headless/UppyContextProvider.js'
|
||||
|
||||
type TEvent = Event & ChangeEvent<HTMLInputElement>
|
||||
|
||||
export function useFileInput(
|
||||
props?: FileInputProps,
|
||||
): FileInputFunctions<TEvent> {
|
||||
const ctx = useUppyContext()
|
||||
|
||||
const fileInput = useMemo(
|
||||
() => createFileInput<TEvent>(ctx, props),
|
||||
// We need every value on props to be memoized to avoid re-creating the file input on every render
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[ctx, props?.accept, props?.multiple],
|
||||
)
|
||||
|
||||
return fileInput
|
||||
}
|
||||
|
|
@ -1,48 +1,46 @@
|
|||
<script context="module" lang="ts">
|
||||
import type Uppy from '@uppy/core';
|
||||
import { createUppyEventAdapter, type UploadStatus } from '@uppy/components'
|
||||
<script module lang="ts">
|
||||
import {
|
||||
createUppyEventAdapter,
|
||||
type UploadStatus,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
|
||||
export const UppyContextKey = 'uppy-context';
|
||||
export type { UppyContext } from '@uppy/components';
|
||||
export const UppyContextKey = 'uppy-context'
|
||||
export type { UppyContext } from '@uppy/components'
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { setContext, onMount } from 'svelte';
|
||||
import { setContext, onMount } from 'svelte'
|
||||
|
||||
export let uppy: Uppy;
|
||||
let { uppy, children } = $props()
|
||||
|
||||
let status: UploadStatus = 'init';
|
||||
let progress = 0;
|
||||
// Create a single reactive context object
|
||||
const contextValue: UppyContext = $state({
|
||||
uppy,
|
||||
status: 'init' as UploadStatus,
|
||||
progress: 0,
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (!uppy) {
|
||||
throw new Error(
|
||||
'ContextProvider: passing `uppy` as a prop is required',
|
||||
);
|
||||
throw new Error('ContextProvider: passing `uppy` as a prop is required')
|
||||
}
|
||||
|
||||
const uppyEventAdapter = createUppyEventAdapter({
|
||||
uppy,
|
||||
onStatusChange: (newStatus: UploadStatus) => {
|
||||
status = newStatus
|
||||
contextValue.status = newStatus
|
||||
},
|
||||
onProgressChange: (newProgress: number) => {
|
||||
progress = newProgress
|
||||
contextValue.progress = newProgress
|
||||
},
|
||||
})
|
||||
|
||||
return () => uppyEventAdapter.cleanup()
|
||||
});
|
||||
})
|
||||
|
||||
// Create a reactive store from our context values
|
||||
$: contextValue = {
|
||||
uppy,
|
||||
status,
|
||||
progress,
|
||||
};
|
||||
|
||||
// Set the context for child components to use
|
||||
$: setContext(UppyContextKey, contextValue);
|
||||
// Set the single context for child components to use
|
||||
setContext(UppyContextKey, contextValue)
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
{@render children()}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
<script lang="ts">
|
||||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { getContext, mount } from 'svelte'
|
||||
import {
|
||||
Dropzone as PreactDropzone,
|
||||
type DropzoneProps,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from '../UppyContextProvider.svelte'
|
||||
|
||||
const props: Omit<DropzoneProps, 'ctx'> = $props()
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
<script lang="ts">
|
||||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { getContext, mount } from 'svelte'
|
||||
import {
|
||||
FilesGrid as PreactFilesGrid,
|
||||
type FilesGridProps,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from '../UppyContextProvider.svelte'
|
||||
|
||||
const props: Omit<FilesGridProps, 'ctx'> = $props()
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
<script lang="ts">
|
||||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { getContext, mount } from 'svelte'
|
||||
import {
|
||||
FilesList as PreactFilesList,
|
||||
type FilesListProps,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from '../UppyContextProvider.svelte'
|
||||
|
||||
const props: Omit<FilesListProps, 'ctx'> = $props()
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
<script lang="ts">
|
||||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { getContext, mount } from 'svelte'
|
||||
import {
|
||||
ProviderIcon as PreactProviderIcon,
|
||||
type ProviderIconProps,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from '../UppyContextProvider.svelte'
|
||||
|
||||
const props: Omit<ProviderIconProps, 'ctx'> = $props()
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
<script lang="ts">
|
||||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { getContext, mount } from 'svelte'
|
||||
import {
|
||||
Thumbnail as PreactThumbnail,
|
||||
type ThumbnailProps,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from '../UppyContextProvider.svelte'
|
||||
|
||||
const props: Omit<ThumbnailProps, 'ctx'> = $props()
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
<script lang="ts">
|
||||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { getContext, mount } from 'svelte'
|
||||
import {
|
||||
UploadButton as PreactUploadButton,
|
||||
type UploadButtonProps,
|
||||
type UppyContext,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { UppyContextKey } from '../UppyContextProvider.svelte'
|
||||
|
||||
const props: Omit<UploadButtonProps, 'ctx'> = $props()
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
|
|
@ -3,4 +3,4 @@ export { default as FilesGrid } from './FilesGrid.svelte'
|
|||
export { default as FilesList } from './FilesList.svelte'
|
||||
export { default as ProviderIcon } from './ProviderIcon.svelte'
|
||||
export { default as Thumbnail } from './Thumbnail.svelte'
|
||||
export { default as UploadButton } from './UploadButton.svelte'
|
||||
export { default as UploadButton } from './UploadButton.svelte'
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { getContext } from 'svelte'
|
||||
import { type UppyContext, type NonNullableUppyContext } from '@uppy/components'
|
||||
|
||||
import { UppyContextKey } from './UppyContextProvider.svelte'
|
||||
|
||||
export function getUppyContext(): NonNullableUppyContext {
|
||||
const ctx = getContext<UppyContext>(UppyContextKey)
|
||||
|
||||
if (!ctx?.uppy) {
|
||||
throw new Error('Component must be called within a UppyContextProvider')
|
||||
}
|
||||
|
||||
return ctx as NonNullableUppyContext // covered by the if statement above
|
||||
}
|
||||
|
|
@ -6,4 +6,8 @@ export { default as StatusBar } from './components/StatusBar.svelte'
|
|||
|
||||
// Headless components
|
||||
export { default as UppyContextProvider } from './components/headless/UppyContextProvider.svelte'
|
||||
export * from './components/headless/index.js'
|
||||
export * from './components/headless/generated/index.js'
|
||||
|
||||
// Hooks
|
||||
export * from './useDropzone.js'
|
||||
export * from './useFileInput.js'
|
||||
|
|
|
|||
52
packages/@uppy/svelte/src/lib/useDropzone.ts
Normal file
52
packages/@uppy/svelte/src/lib/useDropzone.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { getContext } from 'svelte'
|
||||
import {
|
||||
createDropzone,
|
||||
type DropzoneOptions,
|
||||
} from '@uppy/components'
|
||||
|
||||
import { getUppyContext } from './components/headless/uppyContext.js'
|
||||
|
||||
export type SvelteDropzoneReturn = {
|
||||
getRootProps: () => {
|
||||
ondragenter: (event: DragEvent) => void
|
||||
ondragover: (event: DragEvent) => void
|
||||
ondragleave: (event: DragEvent) => void
|
||||
ondrop: (event: DragEvent) => void
|
||||
onclick: () => void
|
||||
}
|
||||
getInputProps: () => {
|
||||
id: string
|
||||
type: 'file'
|
||||
multiple: boolean
|
||||
onchange: (event: Event) => void
|
||||
}
|
||||
}
|
||||
|
||||
export function useDropzone(options?: DropzoneOptions): SvelteDropzoneReturn {
|
||||
const ctx = getUppyContext()
|
||||
const dropzone = createDropzone(ctx, options)
|
||||
|
||||
return {
|
||||
// Only Svelte uses lowercase event names so we want to remap them
|
||||
...dropzone,
|
||||
getRootProps: () => {
|
||||
const { onDragEnter, onDragOver, onDragLeave, onDrop, onClick, ...rest } =
|
||||
dropzone.getRootProps()
|
||||
return {
|
||||
...rest,
|
||||
ondragenter: onDragEnter,
|
||||
ondragover: onDragOver,
|
||||
ondragleave: onDragLeave,
|
||||
ondrop: onDrop,
|
||||
onclick: onClick,
|
||||
}
|
||||
},
|
||||
getInputProps: () => {
|
||||
const { onChange, ...rest } = dropzone.getInputProps()
|
||||
return {
|
||||
...rest,
|
||||
onchange: onChange,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
47
packages/@uppy/svelte/src/lib/useFileInput.ts
Normal file
47
packages/@uppy/svelte/src/lib/useFileInput.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
type FileInputProps,
|
||||
} from '@uppy/components'
|
||||
import { createFileInput } from '@uppy/components'
|
||||
import { getUppyContext } from './components/headless/uppyContext.js'
|
||||
|
||||
export type SvelteFileInputFunctions = {
|
||||
getInputProps: () => {
|
||||
id: string
|
||||
type: 'file'
|
||||
multiple: boolean
|
||||
accept?: string
|
||||
onchange: (event: Event) => void
|
||||
}
|
||||
getButtonProps: () => {
|
||||
type: 'button'
|
||||
onclick: () => void
|
||||
}
|
||||
}
|
||||
|
||||
export function useFileInput(props?: FileInputProps): SvelteFileInputFunctions {
|
||||
const ctx = getUppyContext()
|
||||
|
||||
const fileinput = createFileInput<Event>(
|
||||
ctx,
|
||||
props,
|
||||
)
|
||||
|
||||
return {
|
||||
// Only Svelte uses lowercase event names so we want to remap them
|
||||
...fileinput,
|
||||
getButtonProps: () => {
|
||||
const { onClick, ...rest } = fileinput.getButtonProps()
|
||||
return {
|
||||
...rest,
|
||||
onclick: onClick,
|
||||
}
|
||||
},
|
||||
getInputProps: () => {
|
||||
const { onChange, ...rest } = fileinput.getInputProps()
|
||||
return {
|
||||
...rest,
|
||||
onchange: onChange,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -5,11 +5,12 @@ import {
|
|||
ref,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
inject,
|
||||
} from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type Uppy from '@uppy/core'
|
||||
import { createUppyEventAdapter } from '@uppy/components'
|
||||
import type { UploadStatus } from '@uppy/components'
|
||||
import type { NonNullableUppyContext, UploadStatus } from '@uppy/components'
|
||||
|
||||
export interface UppyContext {
|
||||
uppy: Uppy | undefined
|
||||
|
|
@ -71,3 +72,13 @@ export const UppyContextProvider = defineComponent({
|
|||
})
|
||||
|
||||
export default UppyContextProvider
|
||||
|
||||
export function injectUppyContext(): NonNullableUppyContext {
|
||||
const ctx = inject<UppyContext>(UppyContextSymbol)
|
||||
|
||||
if (!ctx?.uppy) {
|
||||
throw new Error('Component must be called within a UppyContextProvider')
|
||||
}
|
||||
|
||||
return ctx as NonNullableUppyContext // covered by the if-statement above
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
import {
|
||||
Dropzone as PreactDropzone,
|
||||
|
|
@ -6,7 +8,7 @@ import {
|
|||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from './useUppyContext.js'
|
||||
import { useUppyContext } from '../useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<DropzoneProps, 'ctx'>>({
|
||||
name: 'Dropzone',
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
import {
|
||||
FilesGrid as PreactFilesGrid,
|
||||
|
|
@ -6,7 +8,7 @@ import {
|
|||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from './useUppyContext.js'
|
||||
import { useUppyContext } from '../useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<FilesGridProps, 'ctx'>>({
|
||||
name: 'FilesGrid',
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
import {
|
||||
FilesList as PreactFilesList,
|
||||
|
|
@ -6,7 +8,7 @@ import {
|
|||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from './useUppyContext.js'
|
||||
import { useUppyContext } from '../useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<FilesListProps, 'ctx'>>({
|
||||
name: 'FilesList',
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
import {
|
||||
ProviderIcon as PreactProviderIcon,
|
||||
|
|
@ -6,7 +8,7 @@ import {
|
|||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from './useUppyContext.js'
|
||||
import { useUppyContext } from '../useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<ProviderIconProps, 'ctx'>>({
|
||||
name: 'ProviderIcon',
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
import {
|
||||
Thumbnail as PreactThumbnail,
|
||||
|
|
@ -6,7 +8,7 @@ import {
|
|||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from './useUppyContext.js'
|
||||
import { useUppyContext } from '../useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<ThumbnailProps, 'ctx'>>({
|
||||
name: 'Thumbnail',
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
import {
|
||||
UploadButton as PreactUploadButton,
|
||||
|
|
@ -6,7 +8,7 @@ import {
|
|||
import { h as preactH } from 'preact'
|
||||
import { render as preactRender } from 'preact/compat'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from './useUppyContext.js'
|
||||
import { useUppyContext } from '../useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<UploadButtonProps, 'ctx'>>({
|
||||
name: 'UploadButton',
|
||||
|
|
@ -6,4 +6,7 @@ export { default as StatusBar } from './status-bar.js'
|
|||
export { default as FileInput } from './file-input.js'
|
||||
|
||||
export { UppyContextProvider } from './headless/context-provider.js'
|
||||
export * from './headless/index.js'
|
||||
export * from './headless/generated/index.js'
|
||||
|
||||
export * from './useDropzone.js'
|
||||
export * from './useFileInput.js'
|
||||
|
|
|
|||
48
packages/@uppy/vue/src/useDropzone.ts
Normal file
48
packages/@uppy/vue/src/useDropzone.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { createDropzone, type DropzoneOptions } from '@uppy/components'
|
||||
import { injectUppyContext } from './headless/context-provider.js'
|
||||
|
||||
export type VueDropzoneReturn = {
|
||||
getRootProps: () => {
|
||||
ondragenter: (event: DragEvent) => void
|
||||
ondragover: (event: DragEvent) => void
|
||||
ondragleave: (event: DragEvent) => void
|
||||
ondrop: (event: DragEvent) => void
|
||||
onclick: () => void
|
||||
}
|
||||
getInputProps: () => {
|
||||
id: string
|
||||
type: 'file'
|
||||
multiple: boolean
|
||||
onchange: (event: Event) => void
|
||||
}
|
||||
}
|
||||
|
||||
export function useDropzone(options?: DropzoneOptions): VueDropzoneReturn {
|
||||
const ctx = injectUppyContext()
|
||||
|
||||
const dropzone = createDropzone(ctx, options)
|
||||
|
||||
return {
|
||||
// Vue.js uses lowercase event names when using v-bind so we need to remap them
|
||||
...dropzone,
|
||||
getRootProps: () => {
|
||||
const { onDragEnter, onDragOver, onDragLeave, onDrop, onClick, ...rest } =
|
||||
dropzone.getRootProps()
|
||||
return {
|
||||
...rest,
|
||||
ondragenter: onDragEnter,
|
||||
ondragover: onDragOver,
|
||||
ondragleave: onDragLeave,
|
||||
ondrop: onDrop,
|
||||
onclick: onClick,
|
||||
}
|
||||
},
|
||||
getInputProps: () => {
|
||||
const { onChange, ...rest } = dropzone.getInputProps()
|
||||
return {
|
||||
...rest,
|
||||
onchange: onChange,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
42
packages/@uppy/vue/src/useFileInput.ts
Normal file
42
packages/@uppy/vue/src/useFileInput.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { type FileInputProps } from '@uppy/components'
|
||||
import { createFileInput } from '@uppy/components'
|
||||
import { injectUppyContext } from './headless/context-provider.js'
|
||||
|
||||
export type VueFileInputFunctions = {
|
||||
getInputProps: () => {
|
||||
id: string
|
||||
type: 'file'
|
||||
multiple: boolean
|
||||
accept?: string
|
||||
onchange: (event: Event) => void
|
||||
}
|
||||
getButtonProps: () => {
|
||||
type: 'button'
|
||||
onclick: () => void
|
||||
}
|
||||
}
|
||||
|
||||
export function useFileInput(props?: FileInputProps): VueFileInputFunctions {
|
||||
const ctx = injectUppyContext()
|
||||
|
||||
const fileinput = createFileInput<Event>(ctx, props)
|
||||
|
||||
return {
|
||||
// Vue.js uses lowercase event names when using v-bind so we need to remap them
|
||||
...fileinput,
|
||||
getButtonProps: () => {
|
||||
const { onClick, ...rest } = fileinput.getButtonProps()
|
||||
return {
|
||||
...rest,
|
||||
onclick: onClick,
|
||||
}
|
||||
},
|
||||
getInputProps: () => {
|
||||
const { onChange, ...rest } = fileinput.getInputProps()
|
||||
return {
|
||||
...rest,
|
||||
onchange: onChange,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue