Audio, video or file input
Hi !
I was wondering if you intend to do an audio, video, or a more generic file type. Right now, images are supported as ObjectURL, but I wonder why wouldn't we want a similar component using react-dropzone as you currently do but that supports other types of files, most likely sending back a File object.
I can understand custom plugins could be covering the use case, but it doesn't seem you are providing a FileInput primitive within the { Components } from leva/plugin namespace neither, so it's pretty hard to build it while still matching Leva styles (and even less if you modified the default Leva theme).
Hi Enzo, feel free to propose a PR over the existing Image input. That would work right?
@dbismut do you want me to modify the Image input to make it file generic ? Wouldn't that be a breaking for people expecting an image there ? Plus of course the preview square only being useful for images, not other types of files.
There are only 1 thing to be changed in the Image component => the mimetype accepted and not normalizing the value to an ObjectURL, that would already make it support any kind of file.
However I guess we want a generic one and then being able to create other ones like the Image one on top of it.
Hi Enzo, I think we should try to refactor the Image component so that it can support multiple formats, but I agree that we shouldn't break the image API.
So maybe we can have:
useControls(
{ myImage: { image: image.jpg } },
{ myAudio: { audio: sound.mp4 } },
{ myFile : { file: someFile.docx } }
)
And update the preview accordingly?
I will give it a shot in the upcoming days as it becomes a priority for me.
To speed things up, I hacked together a patch using https://github.com/ds300/patch-package that removes the preview and accepts all type of files returning a File value instead.
I will eventually work on a proper solution to contribute to the repo, but this is an escape patch in case you just need whatever generic file as I do.
The patch
diff --git a/node_modules/leva/dist/leva.cjs.dev.js b/node_modules/leva/dist/leva.cjs.dev.js
index e80df53..40a516c 100644
--- a/node_modules/leva/dist/leva.cjs.dev.js
+++ b/node_modules/leva/dist/leva.cjs.dev.js
@@ -691,16 +691,16 @@ const sanitize$1 = v => {
if (v instanceof File) {
try {
- return URL.createObjectURL(v);
+ return v;
} catch (e) {
return undefined;
}
}
- if (typeof v === 'string' && v.indexOf('blob:') === 0) return v;
- throw Error(`Invalid image format [undefined | blob | File].`);
+ if (typeof v === 'string') return v;
+ throw Error(`Invalid image format [undefined | string | File].`);
};
-const schema$1 = (_o, s) => typeof s === 'object' && 'image' in s;
+const schema$1 = (_o, s) => s instanceof File || typeof s === "string";
const normalize$1 = ({
image
}) => {
@@ -719,7 +719,7 @@ var props$1 = /*#__PURE__*/Object.freeze({
const ImageContainer = vectorPlugin.styled('div', {
position: 'relative',
display: 'grid',
- gridTemplateColumns: '$sizes$rowHeight auto 20px',
+ gridTemplateColumns: 'auto auto',
columnGap: '$colGap',
alignItems: 'center'
});
@@ -838,30 +838,11 @@ function ImageComponent() {
isDragAccept
} = reactDropzone.useDropzone({
maxFiles: 1,
- accept: 'image/*',
onDrop
});
return React__default['default'].createElement(vectorPlugin.Row, {
input: true
- }, React__default['default'].createElement(vectorPlugin.Label, null, label), React__default['default'].createElement(ImageContainer, null, React__default['default'].createElement(ImagePreview, {
- ref: popinRef,
- hasImage: !!value,
- onPointerDown: () => !!value && show(),
- onPointerUp: hide,
- style: {
- backgroundImage: value ? `url(${value})` : 'none'
- }
- }), shown && !!value && React__default['default'].createElement(vectorPlugin.Portal, null, React__default['default'].createElement(vectorPlugin.Overlay, {
- onPointerUp: hide,
- style: {
- cursor: 'pointer'
- }
- }), React__default['default'].createElement(ImageLargePreview, {
- ref: wrapperRef,
- style: {
- backgroundImage: `url(${value})`
- }
- })), React__default['default'].createElement(DropZone, getRootProps({
+ }, React__default['default'].createElement(vectorPlugin.Label, null, label), React__default['default'].createElement(ImageContainer, null, React__default['default'].createElement(DropZone, getRootProps({
isDragAccept
}), React__default['default'].createElement("input", getInputProps()), React__default['default'].createElement(Instructions, null, isDragAccept ? 'drop image' : 'click or drop')), React__default['default'].createElement(Remove, {
onClick: clear,
diff --git a/node_modules/leva/dist/leva.cjs.prod.js b/node_modules/leva/dist/leva.cjs.prod.js
index c9c06ba..eb3033b 100644
--- a/node_modules/leva/dist/leva.cjs.prod.js
+++ b/node_modules/leva/dist/leva.cjs.prod.js
@@ -691,16 +691,16 @@ const sanitize$1 = v => {
if (v instanceof File) {
try {
- return URL.createObjectURL(v);
+ return v;
} catch (e) {
return undefined;
}
}
- if (typeof v === 'string' && v.indexOf('blob:') === 0) return v;
- throw Error(`Invalid image format [undefined | blob | File].`);
+ if (typeof v === 'string') return v;
+ throw Error(`Invalid image format [undefined | string | File].`);
};
-const schema$1 = (_o, s) => typeof s === 'object' && 'image' in s;
+const schema$1 = (_o, s) => s instanceof File || typeof s === "string";
const normalize$1 = ({
image
}) => {
@@ -719,7 +719,7 @@ var props$1 = /*#__PURE__*/Object.freeze({
const ImageContainer = vectorPlugin.styled('div', {
position: 'relative',
display: 'grid',
- gridTemplateColumns: '$sizes$rowHeight auto 20px',
+ gridTemplateColumns: 'auto auto',
columnGap: '$colGap',
alignItems: 'center'
});
@@ -838,30 +838,11 @@ function ImageComponent() {
isDragAccept
} = reactDropzone.useDropzone({
maxFiles: 1,
- accept: 'image/*',
onDrop
});
return React__default['default'].createElement(vectorPlugin.Row, {
input: true
- }, React__default['default'].createElement(vectorPlugin.Label, null, label), React__default['default'].createElement(ImageContainer, null, React__default['default'].createElement(ImagePreview, {
- ref: popinRef,
- hasImage: !!value,
- onPointerDown: () => !!value && show(),
- onPointerUp: hide,
- style: {
- backgroundImage: value ? `url(${value})` : 'none'
- }
- }), shown && !!value && React__default['default'].createElement(vectorPlugin.Portal, null, React__default['default'].createElement(vectorPlugin.Overlay, {
- onPointerUp: hide,
- style: {
- cursor: 'pointer'
- }
- }), React__default['default'].createElement(ImageLargePreview, {
- ref: wrapperRef,
- style: {
- backgroundImage: `url(${value})`
- }
- })), React__default['default'].createElement(DropZone, getRootProps({
+ }, React__default['default'].createElement(vectorPlugin.Label, null, label), React__default['default'].createElement(ImageContainer, null, React__default['default'].createElement(DropZone, getRootProps({
isDragAccept
}), React__default['default'].createElement("input", getInputProps()), React__default['default'].createElement(Instructions, null, isDragAccept ? 'drop image' : 'click or drop')), React__default['default'].createElement(Remove, {
onClick: clear,
diff --git a/node_modules/leva/dist/leva.esm.js b/node_modules/leva/dist/leva.esm.js
index 0cdf36d..1d2f2bf 100644
--- a/node_modules/leva/dist/leva.esm.js
+++ b/node_modules/leva/dist/leva.esm.js
@@ -678,16 +678,16 @@ const sanitize$1 = v => {
if (v instanceof File) {
try {
- return URL.createObjectURL(v);
+ return v;
} catch (e) {
return undefined;
}
}
- if (typeof v === 'string' && v.indexOf('blob:') === 0) return v;
- throw Error(`Invalid image format [undefined | blob | File].`);
+ if (typeof v === 'string') return v;
+ throw Error(`Invalid image format [undefined | string | File].`);
};
-const schema$1 = (_o, s) => typeof s === 'object' && 'image' in s;
+const schema$1 = (_o, s) => s instanceof File || typeof s === "string";
const normalize$1 = ({
image
}) => {
@@ -706,7 +706,7 @@ var props$1 = /*#__PURE__*/Object.freeze({
const ImageContainer = styled('div', {
position: 'relative',
display: 'grid',
- gridTemplateColumns: '$sizes$rowHeight auto 20px',
+ gridTemplateColumns: 'auto auto',
columnGap: '$colGap',
alignItems: 'center'
});
@@ -825,30 +825,11 @@ function ImageComponent() {
isDragAccept
} = useDropzone({
maxFiles: 1,
- accept: 'image/*',
onDrop
});
return React.createElement(Row, {
input: true
- }, React.createElement(Label, null, label), React.createElement(ImageContainer, null, React.createElement(ImagePreview, {
- ref: popinRef,
- hasImage: !!value,
- onPointerDown: () => !!value && show(),
- onPointerUp: hide,
- style: {
- backgroundImage: value ? `url(${value})` : 'none'
- }
- }), shown && !!value && React.createElement(Portal, null, React.createElement(Overlay, {
- onPointerUp: hide,
- style: {
- cursor: 'pointer'
- }
- }), React.createElement(ImageLargePreview, {
- ref: wrapperRef,
- style: {
- backgroundImage: `url(${value})`
- }
- })), React.createElement(DropZone, getRootProps({
+ }, React.createElement(Label, null, label), React.createElement(ImageContainer, null, React.createElement(DropZone, getRootProps({
isDragAccept
}), React.createElement("input", getInputProps()), React.createElement(Instructions, null, isDragAccept ? 'drop image' : 'click or drop')), React.createElement(Remove, {
onClick: clear,
diff --git a/node_modules/leva/src/components/Image/Image.tsx b/node_modules/leva/src/components/Image/Image.tsx
index 75d8840..98bce81 100644
--- a/node_modules/leva/src/components/Image/Image.tsx
+++ b/node_modules/leva/src/components/Image/Image.tsx
@@ -8,7 +8,6 @@ import type { ImageProps } from './image-types'
export function ImageComponent() {
const { label, value, onUpdate } = useInputContext<ImageProps>()
- const { popinRef, wrapperRef, shown, show, hide } = usePopin()
const onDrop = useCallback(
(acceptedFiles) => {
@@ -25,26 +24,13 @@ export function ImageComponent() {
[onUpdate]
)
- const { getRootProps, getInputProps, isDragAccept } = useDropzone({ maxFiles: 1, accept: 'image/*', onDrop })
+ const { getRootProps, getInputProps, isDragAccept } = useDropzone({ maxFiles: 1, onDrop })
// TODO fix any in DropZone
return (
<Row input>
<Label>{label}</Label>
<ImageContainer>
- <ImagePreview
- ref={popinRef}
- hasImage={!!value}
- onPointerDown={() => !!value && show()}
- onPointerUp={hide}
- style={{ backgroundImage: value ? `url(${value})` : 'none' }}
- />
- {shown && !!value && (
- <Portal>
- <Overlay onPointerUp={hide} style={{ cursor: 'pointer' }} />
- <ImageLargePreview ref={wrapperRef} style={{ backgroundImage: `url(${value})` }} />
- </Portal>
- )}
<DropZone {...(getRootProps({ isDragAccept }) as any)}>
<input {...getInputProps()} />
<Instructions>{isDragAccept ? 'drop image' : 'click or drop'}</Instructions>
diff --git a/node_modules/leva/src/components/Image/image-plugin.ts b/node_modules/leva/src/components/Image/image-plugin.ts
index 887fecd..75d4c18 100644
--- a/node_modules/leva/src/components/Image/image-plugin.ts
+++ b/node_modules/leva/src/components/Image/image-plugin.ts
@@ -1,19 +1,21 @@
import type { ImageInput } from '../../types'
-export const sanitize = (v: any): string | undefined => {
+export const sanitize = (v: any): File | string | undefined => {
if (v === undefined) return undefined
if (v instanceof File) {
try {
- return URL.createObjectURL(v)
+ return v;
} catch (e) {
return undefined
}
}
- if (typeof v === 'string' && v.indexOf('blob:') === 0) return v
- throw Error(`Invalid image format [undefined | blob | File].`)
+ if(typeof v === "string") {
+ return v;
+ }
+ throw Error(`Invalid image format [undefined | string | File].`)
}
-export const schema = (_o: any, s: any) => typeof s === 'object' && 'image' in s
+export const schema = (_o: any, s: any) => s instanceof File || typeof s === "string";
export const normalize = ({ image }: ImageInput) => {
return { value: image }
diff --git a/node_modules/leva/src/components/Image/image-types.ts b/node_modules/leva/src/components/Image/image-types.ts
index c9f3b96..a3ef8f2 100644
--- a/node_modules/leva/src/components/Image/image-types.ts
+++ b/node_modules/leva/src/components/Image/image-types.ts
@@ -1,3 +1,3 @@
import type { LevaInputProps } from '../../types'
-export type ImageProps = LevaInputProps<string | undefined>
+export type ImageProps = LevaInputProps<File | string | undefined>
diff --git a/node_modules/leva/src/types/public.ts b/node_modules/leva/src/types/public.ts
index 06a55e1..708df91 100644
--- a/node_modules/leva/src/types/public.ts
+++ b/node_modules/leva/src/types/public.ts
@@ -93,7 +93,7 @@ export type Vector3dInput = MergedInputWithSettings<Vector3d, Vector3dSettings>
export type IntervalInput = { value: [number, number]; min: number; max: number }
-export type ImageInput = { image: undefined | string }
+export type ImageInput = { image: undefined | string | File }
type SelectInput = { options: any[] | Record<string, any>; value?: any }
Any news on this?
Hey @TiagoCavalcante , in case you were asking about updates on my side, I'm using leva on a project that is no longer a priority for me. Feel free to use my patch above or contribute yourself 👍🏻
Hello. I'm recently implemented File input plugin, so I can share it.
https://github.com/mishazawa/leva-file-picker