import type { I18n, Translator } from '@uppy/utils' import { Component, type ComponentChild, Fragment, type h } from 'preact' import type { DashboardState, TargetWithRender } from '../Dashboard.js' interface AddFilesProps { i18n: I18n i18nArray: Translator['translateArray'] acquirers: TargetWithRender[] handleInputChange: ( event: h.JSX.TargetedEvent, ) => void maxNumberOfFiles: number | null allowedFileTypes: string[] | null showNativePhotoCameraButton: boolean showNativeVideoCameraButton: boolean nativeCameraFacingMode: 'user' | 'environment' | '' showPanel: (id: string) => void activePickerPanel: DashboardState['activePickerPanel'] disableLocalFiles: boolean fileManagerSelectionType: string note: string | null proudlyDisplayPoweredByUppy: boolean } class AddFiles extends Component { fileInput: HTMLInputElement | null = null folderInput: HTMLInputElement | null = null mobilePhotoFileInput: HTMLInputElement | null = null mobileVideoFileInput: HTMLInputElement | null = null private triggerFileInputClick = () => { this.fileInput?.click() } private triggerFolderInputClick = () => { this.folderInput?.click() } private triggerVideoCameraInputClick = () => { this.mobileVideoFileInput?.click() } private triggerPhotoCameraInputClick = () => { this.mobilePhotoFileInput?.click() } private onFileInputChange = ( event: h.JSX.TargetedEvent, ) => { this.props.handleInputChange(event) // Clear the input so that Chrome/Safari/etc. can detect file section when the same file is repeatedly selected // (see https://github.com/transloadit/uppy/issues/768#issuecomment-2264902758) event.currentTarget.value = '' } private renderHiddenInput = ( isFolder: boolean, refCallback: (ref: HTMLInputElement | null) => void, ) => { return ( ) } private renderHiddenCameraInput = ( type: 'photo' | 'video', nativeCameraFacingMode: 'user' | 'environment' | '', refCallback: (ref: HTMLInputElement | null) => void, ) => { const typeToAccept = { photo: 'image/*', video: 'video/*' } const accept = typeToAccept[type] return ( ) } private renderMyDeviceAcquirer = () => { return (
) } private renderPhotoCamera = () => { return (
) } private renderVideoCamera = () => { return (
) } private renderBrowseButton = ( text: string, onClickFn: (event: Event) => void, ) => { const numberOfAcquirers = this.props.acquirers.length return ( ) } private renderDropPasteBrowseTagline = (numberOfAcquirers: number) => { const browseFiles = this.renderBrowseButton( this.props.i18n('browseFiles'), this.triggerFileInputClick, ) const browseFolders = this.renderBrowseButton( this.props.i18n('browseFolders'), this.triggerFolderInputClick, ) // in order to keep the i18n CamelCase and options lower (as are defaults) we will want to transform a lower // to Camel const lowerFMSelectionType = this.props.fileManagerSelectionType const camelFMSelectionType = lowerFMSelectionType.charAt(0).toUpperCase() + lowerFMSelectionType.slice(1) return (
{this.props.disableLocalFiles ? this.props.i18n('importFiles') : numberOfAcquirers > 0 ? this.props.i18nArray(`dropPasteImport${camelFMSelectionType}`, { browseFiles, browseFolders, browse: browseFiles, }) : this.props.i18nArray(`dropPaste${camelFMSelectionType}`, { browseFiles, browseFolders, browse: browseFiles, })}
) } private [Symbol.for('uppy test: disable unused locale key warning')]() { // Those are actually used in `renderDropPasteBrowseTagline` method. this.props.i18nArray('dropPasteBoth') this.props.i18nArray('dropPasteFiles') this.props.i18nArray('dropPasteFolders') this.props.i18nArray('dropPasteImportBoth') this.props.i18nArray('dropPasteImportFiles') this.props.i18nArray('dropPasteImportFolders') } private renderAcquirer = (acquirer: TargetWithRender) => { return (
) } private renderAcquirers = (acquirers: TargetWithRender[]) => { // Group last two buttons, so we don’t end up with // just one button on a new line const acquirersWithoutLastTwo = [...acquirers] const lastTwoAcquirers = acquirersWithoutLastTwo.splice( acquirers.length - 2, acquirers.length, ) return ( <> {acquirersWithoutLastTwo.map((acquirer) => this.renderAcquirer(acquirer), )} {lastTwoAcquirers.map((acquirer) => this.renderAcquirer(acquirer))} ) } private renderSourcesList = ( acquirers: TargetWithRender[], disableLocalFiles: boolean, ) => { const { showNativePhotoCameraButton, showNativeVideoCameraButton } = this.props type RenderListItem = { key: string; elements: ComponentChild } let list: RenderListItem[] = [] const myDeviceKey = 'myDevice' if (!disableLocalFiles) list.push({ key: myDeviceKey, elements: this.renderMyDeviceAcquirer(), }) if (showNativePhotoCameraButton) list.push({ key: 'nativePhotoCameraButton', elements: this.renderPhotoCamera(), }) if (showNativeVideoCameraButton) list.push({ key: 'nativePhotoCameraButton', elements: this.renderVideoCamera(), }) list.push( ...acquirers.map((acquirer: TargetWithRender) => ({ key: acquirer.id, elements: this.renderAcquirer(acquirer), })), ) // doesn't make sense to show only a lonely "My Device" const hasOnlyMyDevice = list.length === 1 && list[0].key === myDeviceKey if (hasOnlyMyDevice) list = [] // Group last two buttons, so we don’t end up with // just one button on a new line const listWithoutLastTwo = [...list] const lastTwo = listWithoutLastTwo.splice(list.length - 2, list.length) return ( <> {this.renderDropPasteBrowseTagline(list.length)}
{listWithoutLastTwo.map(({ key, elements }) => ( {elements} ))} {lastTwo.map(({ key, elements }) => ( {elements} ))}
) } private renderPoweredByUppy() { const { i18nArray } = this.props const uppyBranding = ( Uppy ) const linkText = i18nArray('poweredBy', { uppy: uppyBranding }) return ( {linkText} ) } render(): ComponentChild { const { showNativePhotoCameraButton, showNativeVideoCameraButton, nativeCameraFacingMode, } = this.props return (
{this.renderHiddenInput(false, (ref) => { this.fileInput = ref })} {this.renderHiddenInput(true, (ref) => { this.folderInput = ref })} {showNativePhotoCameraButton && this.renderHiddenCameraInput( 'photo', nativeCameraFacingMode, (ref) => { this.mobilePhotoFileInput = ref }, )} {showNativeVideoCameraButton && this.renderHiddenCameraInput( 'video', nativeCameraFacingMode, (ref) => { this.mobileVideoFileInput = ref }, )} {this.renderSourcesList( this.props.acquirers, this.props.disableLocalFiles, )}
{this.props.note && (
{this.props.note}
)} {this.props.proudlyDisplayPoweredByUppy && this.renderPoweredByUppy()}
) } } export default AddFiles