diff --git a/packages/@uppy/dashboard/.npmignore b/packages/@uppy/dashboard/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/dashboard/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/dashboard/src/Dashboard.jsx b/packages/@uppy/dashboard/src/Dashboard.tsx similarity index 62% rename from packages/@uppy/dashboard/src/Dashboard.jsx rename to packages/@uppy/dashboard/src/Dashboard.tsx index a3fe0cd4cb..f0ccb7483e 100644 --- a/packages/@uppy/dashboard/src/Dashboard.jsx +++ b/packages/@uppy/dashboard/src/Dashboard.tsx @@ -1,4 +1,14 @@ -import { UIPlugin } from '@uppy/core' +import { + UIPlugin, + type UIPluginOptions, + type UnknownPlugin, + type Uppy, + type UploadResult, + type State, +} from '@uppy/core' +import type { ComponentChild, VNode } from 'preact' +import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts' +import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile' import StatusBar from '@uppy/status-bar' import Informer from '@uppy/informer' import ThumbnailGenerator from '@uppy/thumbnail-generator' @@ -9,124 +19,274 @@ import { defaultPickerIcon } from '@uppy/provider-views' import { nanoid } from 'nanoid/non-secure' import memoizeOne from 'memoize-one' -import * as trapFocus from './utils/trapFocus.js' -import createSuperFocus from './utils/createSuperFocus.js' -import DashboardUI from './components/Dashboard.jsx' +import * as trapFocus from './utils/trapFocus.ts' +import createSuperFocus from './utils/createSuperFocus.ts' +import DashboardUI from './components/Dashboard.tsx' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../package.json' -import locale from './locale.js' +import locale from './locale.ts' + +type GenericEventCallback = () => void +export type DashboardFileEditStartCallback = ( + file?: UppyFile, +) => void +export type DashboardFileEditCompleteCallback< + M extends Meta, + B extends Body, +> = (file?: UppyFile) => void +export type DashboardShowPlanelCallback = (id: string) => void +declare module '@uppy/core' { + export interface UppyEventMap { + 'dashboard:modal-open': GenericEventCallback + 'dashboard:modal-closed': GenericEventCallback + 'dashboard:show-panel': DashboardShowPlanelCallback + 'dashboard:file-edit-start': DashboardFileEditStartCallback + 'dashboard:file-edit-complete': DashboardFileEditCompleteCallback + 'dashboard:close-panel': (id: string | undefined) => void + 'restore-canceled': GenericEventCallback + } +} + +interface PromiseWithResolvers { + promise: Promise + resolve: (value: T | PromiseLike) => void + reject: (reason?: any) => void +} -const memoize = memoizeOne.default || memoizeOne +const memoize = ((memoizeOne as any).default as false) || memoizeOne const TAB_KEY = 9 const ESC_KEY = 27 -function createPromise () { - const o = {} - o.promise = new Promise((resolve, reject) => { +function createPromise(): PromiseWithResolvers { + const o = {} as PromiseWithResolvers + o.promise = new Promise((resolve, reject) => { o.resolve = resolve o.reject = reject }) return o } +type FieldRenderOptions = { + value: string + onChange: (newVal: string) => void + fieldCSSClasses: { text: string } + required: boolean + form: string +} + +type PreactRender = ( + node: any, + params: Record | null, + ...children: any[] +) => VNode + +interface MetaField { + id: string + name: string + placeholder?: string + render?: (field: FieldRenderOptions, h: PreactRender) => VNode +} + +interface Target { + id: string + name: string + type: string +} + +interface TargetWithRender extends Target { + icon: ComponentChild + render: () => ComponentChild +} + +interface DashboardState { + targets: Target[] + activePickerPanel: Target | undefined + showAddFilesPanel: boolean + activeOverlayType: string | null + fileCardFor: string | null + showFileEditor: boolean + metaFields?: MetaField[] | ((file: UppyFile) => MetaField[]) + [key: string]: unknown +} + +interface DashboardOptions + extends UIPluginOptions { + animateOpenClose?: boolean + browserBackButtonClose?: boolean + closeAfterFinish?: boolean + singleFileFullScreen?: boolean + closeModalOnClickOutside?: boolean + disableInformer?: boolean + disablePageScrollWhenModalOpen?: boolean + disableStatusBar?: boolean + disableThumbnailGenerator?: boolean + height?: string | number + thumbnailWidth?: number + thumbnailHeight?: number + thumbnailType?: string + nativeCameraFacingMode?: ConstrainDOMString + waitForThumbnailsBeforeUpload?: boolean + defaultPickerIcon?: typeof defaultPickerIcon + hideCancelButton?: boolean + hidePauseResumeButton?: boolean + hideProgressAfterFinish?: boolean + hideRetryButton?: boolean + hideUploadButton?: boolean + inline?: boolean + metaFields?: MetaField[] | ((file: UppyFile) => MetaField[]) + note?: string | null + plugins?: string[] + fileManagerSelectionType?: 'files' | 'folders' | 'both' + proudlyDisplayPoweredByUppy?: boolean + showLinkToFileUploadResult?: boolean + showProgressDetails?: boolean + showSelectedFiles?: boolean + showRemoveButtonAfterComplete?: boolean + showNativePhotoCameraButton?: boolean + showNativeVideoCameraButton?: boolean + theme?: 'auto' | 'dark' | 'light' + trigger?: string + width?: string | number + autoOpenFileEditor?: boolean + disabled?: boolean + disableLocalFiles?: boolean + onRequestCloseModal?: () => void + doneButtonHandler?: () => void + onDragOver?: (event: DragEvent) => void + onDragLeave?: (event: DragEvent) => void + onDrop?: (event: DragEvent) => void +} + +// set default options, must be kept in sync with packages/@uppy/react/src/DashboardModal.js +const defaultOptions = { + target: 'body', + metaFields: [], + inline: false, + width: 750, + height: 550, + thumbnailWidth: 280, + thumbnailType: 'image/jpeg', + waitForThumbnailsBeforeUpload: false, + defaultPickerIcon, + showLinkToFileUploadResult: false, + showProgressDetails: false, + hideUploadButton: false, + hideCancelButton: false, + hideRetryButton: false, + hidePauseResumeButton: false, + hideProgressAfterFinish: false, + note: null, + closeModalOnClickOutside: false, + closeAfterFinish: false, + singleFileFullScreen: true, + disableStatusBar: false, + disableInformer: false, + disableThumbnailGenerator: false, + disablePageScrollWhenModalOpen: true, + animateOpenClose: true, + fileManagerSelectionType: 'files', + proudlyDisplayPoweredByUppy: true, + showSelectedFiles: true, + showRemoveButtonAfterComplete: false, + browserBackButtonClose: false, + showNativePhotoCameraButton: false, + showNativeVideoCameraButton: false, + theme: 'light', + autoOpenFileEditor: false, + disabled: false, + disableLocalFiles: false, + + // Dynamic default options, they have to be defined in the constructor (because + // they require access to the `this` keyword), but we still want them to + // appear in the default options so TS knows they'll be defined. + doneButtonHandler: null as any, + onRequestCloseModal: null as any, +} satisfies Partial> + /** * Dashboard UI with previews, metadata editing, tabs for various services and more */ -export default class Dashboard extends UIPlugin { +export default class Dashboard extends UIPlugin< + DefinePluginOpts, keyof typeof defaultOptions>, + M, + B, + DashboardState +> { static VERSION = packageJson.version - #disabledNodes = null + #disabledNodes: HTMLElement[] | null + + private modalName = `uppy-Dashboard-${nanoid()}` + + private superFocus = createSuperFocus() + + private ifFocusedOnUppyRecently = false + + private dashboardIsDisabled: boolean + + private savedScrollPosition: number + + private savedActiveElement: HTMLElement - constructor (uppy, opts) { - super(uppy, opts) + private resizeObserver: ResizeObserver + + private darkModeMediaQuery: MediaQueryList | null + + // Timeouts + private makeDashboardInsidesVisibleAnywayTimeout: ReturnType< + typeof setTimeout + > + + private removeDragOverClassTimeout: ReturnType + + constructor(uppy: Uppy, opts?: DashboardOptions) { + super(uppy, { ...defaultOptions, ...opts }) this.id = this.opts.id || 'Dashboard' this.title = 'Dashboard' this.type = 'orchestrator' - this.modalName = `uppy-Dashboard-${nanoid()}` this.defaultLocale = locale - // set default options, must be kept in sync with packages/@uppy/react/src/DashboardModal.js - const defaultOptions = { - target: 'body', - metaFields: [], - trigger: null, - inline: false, - width: 750, - height: 550, - thumbnailWidth: 280, - thumbnailType: 'image/jpeg', - waitForThumbnailsBeforeUpload: false, - defaultPickerIcon, - showLinkToFileUploadResult: false, - showProgressDetails: false, - hideUploadButton: false, - hideCancelButton: false, - hideRetryButton: false, - hidePauseResumeButton: false, - hideProgressAfterFinish: false, - doneButtonHandler: () => { - this.uppy.clearUploadedFiles() - this.requestCloseModal() - }, - note: null, - closeModalOnClickOutside: false, - closeAfterFinish: false, - singleFileFullScreen: true, - disableStatusBar: false, - disableInformer: false, - disableThumbnailGenerator: false, - disablePageScrollWhenModalOpen: true, - animateOpenClose: true, - fileManagerSelectionType: 'files', - proudlyDisplayPoweredByUppy: true, - onRequestCloseModal: () => this.closeModal(), - showSelectedFiles: true, - showRemoveButtonAfterComplete: false, - browserBackButtonClose: false, - showNativePhotoCameraButton: false, - showNativeVideoCameraButton: false, - theme: 'light', - autoOpenFileEditor: false, - disabled: false, - disableLocalFiles: false, + // Dynamic default options: + this.opts.doneButtonHandler ??= () => { + this.uppy.clearUploadedFiles() + this.requestCloseModal() } - - // merge default options with the ones set by user - this.opts = { ...defaultOptions, ...opts } + this.opts.onRequestCloseModal ??= () => this.closeModal() this.i18nInit() - - this.superFocus = createSuperFocus() - this.ifFocusedOnUppyRecently = false - - // Timeouts - this.makeDashboardInsidesVisibleAnywayTimeout = null - this.removeDragOverClassTimeout = null } - removeTarget = (plugin) => { + removeTarget = (plugin: UnknownPlugin): void => { const pluginState = this.getPluginState() // filter out the one we want to remove - const newTargets = pluginState.targets.filter(target => target.id !== plugin.id) + const newTargets = pluginState.targets.filter( + (target) => target.id !== plugin.id, + ) this.setPluginState({ targets: newTargets, }) } - addTarget = (plugin) => { + addTarget = (plugin: UnknownPlugin): HTMLElement | null => { const callerPluginId = plugin.id || plugin.constructor.name - const callerPluginName = plugin.title || callerPluginId + const callerPluginName = + (plugin as any as { title: string }).title || callerPluginId const callerPluginType = plugin.type - if (callerPluginType !== 'acquirer' - && callerPluginType !== 'progressindicator' - && callerPluginType !== 'editor') { - const msg = 'Dashboard: can only be targeted by plugins of types: acquirer, progressindicator, editor' + if ( + callerPluginType !== 'acquirer' && + callerPluginType !== 'progressindicator' && + callerPluginType !== 'editor' + ) { + const msg = + 'Dashboard: can only be targeted by plugins of types: acquirer, progressindicator, editor' this.uppy.log(msg, 'error') - return undefined + return null } const target = { @@ -146,35 +306,37 @@ export default class Dashboard extends UIPlugin { return this.el } - hideAllPanels = () => { + hideAllPanels = (): void => { const state = this.getPluginState() const update = { - activePickerPanel: false, + activePickerPanel: undefined, showAddFilesPanel: false, activeOverlayType: null, fileCardFor: null, showFileEditor: false, } - if (state.activePickerPanel === update.activePickerPanel - && state.showAddFilesPanel === update.showAddFilesPanel - && state.showFileEditor === update.showFileEditor - && state.activeOverlayType === update.activeOverlayType) { + if ( + state.activePickerPanel === update.activePickerPanel && + state.showAddFilesPanel === update.showAddFilesPanel && + state.showFileEditor === update.showFileEditor && + state.activeOverlayType === update.activeOverlayType + ) { // avoid doing a state update if nothing changed return } this.setPluginState(update) - this.uppy.emit('dashboard:close-panel', state.activePickerPanel.id) + this.uppy.emit('dashboard:close-panel', state.activePickerPanel?.id) } - showPanel = (id) => { + showPanel = (id: string): void => { const { targets } = this.getPluginState() - const activePickerPanel = targets.filter((target) => { + const activePickerPanel = targets.find((target) => { return target.type === 'acquirer' && target.id === id - })[0] + }) this.setPluginState({ activePickerPanel, @@ -184,16 +346,16 @@ export default class Dashboard extends UIPlugin { this.uppy.emit('dashboard:show-panel', id) } - canEditFile = (file) => { + private canEditFile = (file: UppyFile): boolean => { const { targets } = this.getPluginState() const editors = this.#getEditors(targets) - return editors.some((target) => ( - this.uppy.getPlugin(target.id).canEditFile(file) - )) + return editors.some((target) => + (this.uppy.getPlugin(target.id) as any).canEditFile(file), + ) } - openFileEditor = (file) => { + openFileEditor = (file: UppyFile): void => { const { targets } = this.getPluginState() const editors = this.#getEditors(targets) @@ -204,45 +366,45 @@ export default class Dashboard extends UIPlugin { }) editors.forEach((editor) => { - this.uppy.getPlugin(editor.id).selectFile(file) + ;(this.uppy.getPlugin(editor.id) as any).selectFile(file) }) } - closeFileEditor = () => { + closeFileEditor = (): void => { const { metaFields } = this.getPluginState() const isMetaEditorEnabled = metaFields && metaFields.length > 0 if (isMetaEditorEnabled) { this.setPluginState({ showFileEditor: false, - activeOverlayType: 'FileCard' + activeOverlayType: 'FileCard', }) } else { this.setPluginState({ showFileEditor: false, fileCardFor: null, - activeOverlayType: 'AddFiles' + activeOverlayType: 'AddFiles', }) } } - saveFileEditor = () => { + saveFileEditor = (): void => { const { targets } = this.getPluginState() const editors = this.#getEditors(targets) editors.forEach((editor) => { - this.uppy.getPlugin(editor.id).save() + ;(this.uppy.getPlugin(editor.id) as any).save() }) this.closeFileEditor() } - openModal = () => { - const { promise, resolve } = createPromise() + openModal = (): Promise => { + const { promise, resolve } = createPromise() // save scroll position this.savedScrollPosition = window.pageYOffset // save active element, so we can restore focus when modal is closed - this.savedActiveElement = document.activeElement + this.savedActiveElement = document.activeElement as HTMLElement if (this.opts.disablePageScrollWhenModalOpen) { document.body.classList.add('uppy-Dashboard-isFixed') @@ -253,10 +415,10 @@ export default class Dashboard extends UIPlugin { this.setPluginState({ isHidden: false, }) - this.el.removeEventListener('animationend', handler, false) + this.el!.removeEventListener('animationend', handler, false) resolve() } - this.el.addEventListener('animationend', handler, false) + this.el!.addEventListener('animationend', handler, false) } else { this.setPluginState({ isHidden: false, @@ -276,11 +438,9 @@ export default class Dashboard extends UIPlugin { return promise } - closeModal = (opts = {}) => { - const { - // Whether the modal is being closed by the user (`true`) or by other means (e.g. browser back button) - manualClose = true, - } = opts + closeModal = (opts?: { manualClose: boolean }): void | Promise => { + // Whether the modal is being closed by the user (`true`) or by other means (e.g. browser back button) + const manualClose = opts?.manualClose ?? true const { isHidden, isClosing } = this.getPluginState() if (isHidden || isClosing) { @@ -288,7 +448,7 @@ export default class Dashboard extends UIPlugin { return undefined } - const { promise, resolve } = createPromise() + const { promise, resolve } = createPromise() if (this.opts.disablePageScrollWhenModalOpen) { document.body.classList.remove('uppy-Dashboard-isFixed') @@ -307,10 +467,10 @@ export default class Dashboard extends UIPlugin { this.superFocus.cancel() this.savedActiveElement.focus() - this.el.removeEventListener('animationend', handler, false) + this.el!.removeEventListener('animationend', handler, false) resolve() } - this.el.addEventListener('animationend', handler, false) + this.el!.addEventListener('animationend', handler, false) } else { this.setPluginState({ isHidden: true, @@ -342,18 +502,18 @@ export default class Dashboard extends UIPlugin { return promise } - isModalOpen = () => { + isModalOpen = (): boolean => { return !this.getPluginState().isHidden || false } - requestCloseModal = () => { + private requestCloseModal = (): void | Promise => { if (this.opts.onRequestCloseModal) { return this.opts.onRequestCloseModal() } return this.closeModal() } - setDarkModeCapability = (isDarkModeOn) => { + setDarkModeCapability = (isDarkModeOn: boolean): void => { const { capabilities } = this.uppy.getState() this.uppy.setState({ capabilities: { @@ -363,13 +523,13 @@ export default class Dashboard extends UIPlugin { }) } - handleSystemDarkModeChange = (event) => { + private handleSystemDarkModeChange = (event: MediaQueryListEvent) => { const isDarkModeOnNow = event.matches this.uppy.log(`[Dashboard] Dark mode is ${isDarkModeOnNow ? 'on' : 'off'}`) this.setDarkModeCapability(isDarkModeOnNow) } - toggleFileCard = (show, fileID) => { + private toggleFileCard = (show: boolean, fileID: string) => { const file = this.uppy.getFile(fileID) if (show) { this.uppy.emit('dashboard:file-edit-start', file) @@ -383,14 +543,14 @@ export default class Dashboard extends UIPlugin { }) } - toggleAddFilesPanel = (show) => { + private toggleAddFilesPanel = (show: boolean) => { this.setPluginState({ showAddFilesPanel: show, activeOverlayType: show ? 'AddFiles' : null, }) } - addFiles = (files) => { + addFiles = (files: File[]): void => { const descriptors = files.map((file) => ({ source: this.id, name: file.name, @@ -399,8 +559,9 @@ export default class Dashboard extends UIPlugin { meta: { // path of the file relative to the ancestor directory the user selected. // e.g. 'docs/Old Prague/airbnb.pdf' - relativePath: file.relativePath || file.webkitRelativePath || null, - }, + relativePath: + (file as any).relativePath || file.webkitRelativePath || null, + } as any as M, })) try { @@ -416,7 +577,7 @@ export default class Dashboard extends UIPlugin { // ___Why not apply visibility property to .uppy-Dashboard-inner? // Because ideally, acc to specs, ResizeObserver should see invisible elements as of width 0. So even though applying // invisibility to .uppy-Dashboard-inner works now, it may not work in the future. - startListeningToResize = () => { + private startListeningToResize = () => { // Watch for Dashboard container (`.uppy-Dashboard-inner`) resize // and update containerWidth/containerHeight in plugin state accordingly. // Emits first event on initialization. @@ -430,20 +591,26 @@ export default class Dashboard extends UIPlugin { areInsidesReadyToBeVisible: true, }) }) - this.resizeObserver.observe(this.el.querySelector('.uppy-Dashboard-inner')) + this.resizeObserver.observe( + this.el!.querySelector('.uppy-Dashboard-inner')!, + ) // If ResizeObserver fails to emit an event telling us what size to use - default to the mobile view this.makeDashboardInsidesVisibleAnywayTimeout = setTimeout(() => { const pluginState = this.getPluginState() const isModalAndClosed = !this.opts.inline && pluginState.isHidden - if (// We might want to enable this in the future + if ( + // We might want to enable this in the future // if ResizeObserver hasn't yet fired, - !pluginState.areInsidesReadyToBeVisible + !pluginState.areInsidesReadyToBeVisible && // and it's not due to the modal being closed - && !isModalAndClosed + !isModalAndClosed ) { - this.uppy.log('[Dashboard] resize event didn’t fire on time: defaulted to mobile layout', 'warning') + this.uppy.log( + '[Dashboard] resize event didn’t fire on time: defaulted to mobile layout', + 'warning', + ) this.setPluginState({ areInsidesReadyToBeVisible: true, @@ -452,7 +619,7 @@ export default class Dashboard extends UIPlugin { }, 1000) } - stopListeningToResize = () => { + private stopListeningToResize = () => { this.resizeObserver.disconnect() clearTimeout(this.makeDashboardInsidesVisibleAnywayTimeout) @@ -460,8 +627,8 @@ export default class Dashboard extends UIPlugin { // Records whether we have been interacting with uppy right now, // which is then used to determine whether state updates should trigger a refocusing. - recordIfFocusedOnUppyRecently = (event) => { - if (this.el.contains(event.target)) { + private recordIfFocusedOnUppyRecently = (event: Event) => { + if (this.el!.contains(event.target as HTMLElement)) { this.ifFocusedOnUppyRecently = true } else { this.ifFocusedOnUppyRecently = false @@ -472,7 +639,7 @@ export default class Dashboard extends UIPlugin { } } - disableInteractiveElements = (disable) => { + private disableInteractiveElements = (disable: boolean) => { const NODES_TO_DISABLE = [ 'a[href]', 'input:not([disabled])', @@ -482,8 +649,11 @@ export default class Dashboard extends UIPlugin { '[role="button"]:not([disabled])', ] - const nodesToDisable = this.#disabledNodes ?? toArray(this.el.querySelectorAll(NODES_TO_DISABLE)) - .filter(node => !node.classList.contains('uppy-Dashboard-close')) + const nodesToDisable = + this.#disabledNodes ?? + toArray(this.el!.querySelectorAll(NODES_TO_DISABLE as any)).filter( + (node) => !node.classList.contains('uppy-Dashboard-close'), + ) for (const node of nodesToDisable) { // Links can’t have `disabled` attr, so we use `aria-disabled` for a11y @@ -503,24 +673,27 @@ export default class Dashboard extends UIPlugin { this.dashboardIsDisabled = disable } - updateBrowserHistory = () => { + private updateBrowserHistory = () => { // Ensure history state does not already contain our modal name to avoid double-pushing // eslint-disable-next-line no-restricted-globals if (!history.state?.[this.modalName]) { // Push to history so that the page is not lost on browser back button press // eslint-disable-next-line no-restricted-globals - history.pushState({ - // eslint-disable-next-line no-restricted-globals - ...history.state, - [this.modalName]: true, - }, '') + history.pushState( + { + // eslint-disable-next-line no-restricted-globals + ...history.state, + [this.modalName]: true, + }, + '', + ) } // Listen for back button presses window.addEventListener('popstate', this.handlePopState, false) } - handlePopState = (event) => { + private handlePopState = (event: PopStateEvent) => { // Close the modal if the history state no longer contains our modal name if (this.isModalOpen() && (!event.state || !event.state[this.modalName])) { this.closeModal({ manualClose: false }) @@ -538,44 +711,49 @@ export default class Dashboard extends UIPlugin { } } - handleKeyDownInModal = (event) => { + private handleKeyDownInModal = (event: KeyboardEvent) => { // close modal on esc key press - if (event.keyCode === ESC_KEY) this.requestCloseModal(event) + if (event.keyCode === ESC_KEY) this.requestCloseModal() // trap focus on tab key press - if (event.keyCode === TAB_KEY) trapFocus.forModal(event, this.getPluginState().activeOverlayType, this.el) + if (event.keyCode === TAB_KEY) + trapFocus.forModal( + event, + this.getPluginState().activeOverlayType, + this.el, + ) } - handleClickOutside = () => { + private handleClickOutside = () => { if (this.opts.closeModalOnClickOutside) this.requestCloseModal() } - handlePaste = (event) => { + private handlePaste = (event: ClipboardEvent) => { // Let any acquirer plugin (Url/Webcam/etc.) handle pastes to the root this.uppy.iteratePlugins((plugin) => { if (plugin.type === 'acquirer') { // Every Plugin with .type acquirer can define handleRootPaste(event) - plugin.handleRootPaste?.(event) + ;(plugin as any).handleRootPaste?.(event) } }) // Add all dropped files - const files = toArray(event.clipboardData.files) + const files = toArray(event.clipboardData!.files) if (files.length > 0) { this.uppy.log('[Dashboard] Files pasted') this.addFiles(files) } } - handleInputChange = (event) => { + private handleInputChange = (event: InputEvent) => { event.preventDefault() - const files = toArray(event.target.files) + const files = toArray((event.target as HTMLInputElement).files!) if (files.length > 0) { this.uppy.log('[Dashboard] Files selected through input') this.addFiles(files) } } - handleDragOver = (event) => { + private handleDragOver = (event: DragEvent) => { event.preventDefault() event.stopPropagation() @@ -584,7 +762,7 @@ export default class Dashboard extends UIPlugin { const canSomePluginHandleRootDrop = () => { let somePluginCanHandleRootDrop = true this.uppy.iteratePlugins((plugin) => { - if (plugin.canHandleRootDrop?.(event)) { + if ((plugin as any).canHandleRootDrop?.(event)) { somePluginCanHandleRootDrop = true } }) @@ -593,23 +771,24 @@ export default class Dashboard extends UIPlugin { // Check if the "type" of the datatransfer object includes files const doesEventHaveFiles = () => { - const { types } = event.dataTransfer - return types.some(type => type === 'Files') + const { types } = event.dataTransfer! + return types.some((type) => type === 'Files') } // Deny drop, if no plugins can handle datatransfer, there are no files, // or when opts.disabled is set, or new uploads are not allowed - const somePluginCanHandleRootDrop = canSomePluginHandleRootDrop(event) - const hasFiles = doesEventHaveFiles(event) + const somePluginCanHandleRootDrop = canSomePluginHandleRootDrop() + const hasFiles = doesEventHaveFiles() if ( - (!somePluginCanHandleRootDrop && !hasFiles) - || this.opts.disabled + (!somePluginCanHandleRootDrop && !hasFiles) || + this.opts.disabled || // opts.disableLocalFiles should only be taken into account if no plugins // can handle the datatransfer - || (this.opts.disableLocalFiles && (hasFiles || !somePluginCanHandleRootDrop)) - || !this.uppy.getState().allowNewUpload + (this.opts.disableLocalFiles && + (hasFiles || !somePluginCanHandleRootDrop)) || + !this.uppy.getState().allowNewUpload ) { - event.dataTransfer.dropEffect = 'none' // eslint-disable-line no-param-reassign + event.dataTransfer!.dropEffect = 'none' // eslint-disable-line no-param-reassign clearTimeout(this.removeDragOverClassTimeout) return } @@ -617,7 +796,7 @@ export default class Dashboard extends UIPlugin { // Add a small (+) icon on drop // (and prevent browsers from interpreting this as files being _moved_ into the // browser, https://github.com/transloadit/uppy/issues/1978). - event.dataTransfer.dropEffect = 'copy' // eslint-disable-line no-param-reassign + event.dataTransfer!.dropEffect = 'copy' // eslint-disable-line no-param-reassign clearTimeout(this.removeDragOverClassTimeout) this.setPluginState({ isDraggingOver: true }) @@ -625,7 +804,7 @@ export default class Dashboard extends UIPlugin { this.opts.onDragOver?.(event) } - handleDragLeave = (event) => { + private handleDragLeave = (event: DragEvent) => { event.preventDefault() event.stopPropagation() @@ -639,7 +818,7 @@ export default class Dashboard extends UIPlugin { this.opts.onDragLeave?.(event) } - handleDrop = async (event) => { + private handleDrop = async (event: DragEvent) => { event.preventDefault() event.stopPropagation() @@ -651,13 +830,13 @@ export default class Dashboard extends UIPlugin { this.uppy.iteratePlugins((plugin) => { if (plugin.type === 'acquirer') { // Every Plugin with .type acquirer can define handleRootDrop(event) - plugin.handleRootDrop?.(event) + ;(plugin as any).handleRootDrop?.(event) } }) // Add all dropped files let executedDropErrorOnce = false - const logDropError = (error) => { + const logDropError = (error: any) => { this.uppy.log(error, 'error') // In practice all drop errors are most likely the same, @@ -671,7 +850,7 @@ export default class Dashboard extends UIPlugin { this.uppy.log('[Dashboard] Processing dropped files') // Add all dropped files - const files = await getDroppedFiles(event.dataTransfer, { logDropError }) + const files = await getDroppedFiles(event.dataTransfer!, { logDropError }) if (files.length > 0) { this.uppy.log('[Dashboard] Files dropped') this.addFiles(files) @@ -680,7 +859,7 @@ export default class Dashboard extends UIPlugin { this.opts.onDrop?.(event) } - handleRequestThumbnail = (file) => { + private handleRequestThumbnail = (file: UppyFile) => { if (!this.opts.waitForThumbnailsBeforeUpload) { this.uppy.emit('thumbnail:request', file) } @@ -690,15 +869,20 @@ export default class Dashboard extends UIPlugin { * We cancel thumbnail requests when a file item component unmounts to avoid * clogging up the queue when the user scrolls past many elements. */ - handleCancelThumbnail = (file) => { + private handleCancelThumbnail = (file: UppyFile) => { if (!this.opts.waitForThumbnailsBeforeUpload) { this.uppy.emit('thumbnail:cancel', file) } } - handleKeyDownInInline = (event) => { + private handleKeyDownInInline = (event: KeyboardEvent) => { // Trap focus on tab key press. - if (event.keyCode === TAB_KEY) trapFocus.forInline(event, this.getPluginState().activeOverlayType, this.el) + if (event.keyCode === TAB_KEY) + trapFocus.forInline( + event, + this.getPluginState().activeOverlayType, + this.el, + ) } // ___Why do we listen to the 'paste' event on a document instead of onPaste={props.handlePaste} prop, @@ -710,21 +894,21 @@ export default class Dashboard extends UIPlugin { // Because if we click on the 'Drop files here' caption e.g., `document.activeElement` will be 'body'. Which means our // standard determination of whether we're pasting into our Uppy instance won't work. // => Therefore, we need a traditional onPaste={props.handlePaste} handler too. - handlePasteOnBody = (event) => { - const isFocusInOverlay = this.el.contains(document.activeElement) + private handlePasteOnBody = (event: ClipboardEvent) => { + const isFocusInOverlay = this.el!.contains(document.activeElement) if (isFocusInOverlay) { this.handlePaste(event) } } - handleComplete = ({ failed }) => { - if (this.opts.closeAfterFinish && failed.length === 0) { + private handleComplete = ({ failed }: UploadResult) => { + if (this.opts.closeAfterFinish && !failed?.length) { // All uploads are done this.requestCloseModal() } } - handleCancelRestore = () => { + private handleCancelRestore = () => { this.uppy.emit('restore-canceled') } @@ -737,19 +921,23 @@ export default class Dashboard extends UIPlugin { const files = this.uppy.getFiles() if (files.length === 1) { - const thumbnailGenerator = this.uppy.getPlugin(`${this.id}:ThumbnailGenerator`) + const thumbnailGenerator = this.uppy.getPlugin( + `${this.id}:ThumbnailGenerator`, + ) as ThumbnailGenerator | undefined thumbnailGenerator?.setOptions({ thumbnailWidth: LARGE_THUMBNAIL }) const fileForThumbnail = { ...files[0], preview: undefined } - thumbnailGenerator.requestThumbnail(fileForThumbnail).then(() => { - thumbnailGenerator?.setOptions({ thumbnailWidth: this.opts.thumbnailWidth }) + thumbnailGenerator?.requestThumbnail(fileForThumbnail).then(() => { + thumbnailGenerator?.setOptions({ + thumbnailWidth: this.opts.thumbnailWidth, + }) }) } } - #openFileEditorWhenFilesAdded = (files) => { + #openFileEditorWhenFilesAdded = (files: UppyFile[]) => { const firstFile = files[0] - const {metaFields} = this.getPluginState() + const { metaFields } = this.getPluginState() const isMetaEditorEnabled = metaFields && metaFields.length > 0 const isFileEditorEnabled = this.canEditFile(firstFile) @@ -760,14 +948,19 @@ export default class Dashboard extends UIPlugin { } } - initEvents = () => { + initEvents = (): void => { // Modal open button if (this.opts.trigger && !this.opts.inline) { const showModalTrigger = findAllDOMElements(this.opts.trigger) if (showModalTrigger) { - showModalTrigger.forEach(trigger => trigger.addEventListener('click', this.openModal)) + showModalTrigger.forEach((trigger) => + trigger.addEventListener('click', this.openModal), + ) } else { - this.uppy.log('Dashboard modal trigger not found. Make sure `trigger` is set in Dashboard options, unless you are planning to call `dashboard.openModal()` method yourself', 'warning') + this.uppy.log( + 'Dashboard modal trigger not found. Make sure `trigger` is set in Dashboard options, unless you are planning to call `dashboard.openModal()` method yourself', + 'warning', + ) } } @@ -789,7 +982,7 @@ export default class Dashboard extends UIPlugin { document.addEventListener('click', this.recordIfFocusedOnUppyRecently, true) if (this.opts.inline) { - this.el.addEventListener('keydown', this.handleKeyDownInInline) + this.el!.addEventListener('keydown', this.handleKeyDownInInline) } if (this.opts.autoOpenFileEditor) { @@ -797,10 +990,12 @@ export default class Dashboard extends UIPlugin { } } - removeEvents = () => { + removeEvents = (): void => { const showModalTrigger = findAllDOMElements(this.opts.trigger) if (!this.opts.inline && showModalTrigger) { - showModalTrigger.forEach(trigger => trigger.removeEventListener('click', this.openModal)) + showModalTrigger.forEach((trigger) => + trigger.removeEventListener('click', this.openModal), + ) } this.stopListeningToResize() @@ -820,7 +1015,7 @@ export default class Dashboard extends UIPlugin { document.removeEventListener('click', this.recordIfFocusedOnUppyRecently) if (this.opts.inline) { - this.el.removeEventListener('keydown', this.handleKeyDownInInline) + this.el!.removeEventListener('keydown', this.handleKeyDownInInline) } if (this.opts.autoOpenFileEditor) { @@ -828,22 +1023,23 @@ export default class Dashboard extends UIPlugin { } } - superFocusOnEachUpdate = () => { - const isFocusInUppy = this.el.contains(document.activeElement) + private superFocusOnEachUpdate = () => { + const isFocusInUppy = this.el!.contains(document.activeElement) // When focus is lost on the page (== focus is on body for most browsers, or focus is null for IE11) - const isFocusNowhere = document.activeElement === document.body || document.activeElement === null + const isFocusNowhere = + document.activeElement === document.body || + document.activeElement === null const isInformerHidden = this.uppy.getState().info.length === 0 const isModal = !this.opts.inline if ( // If update is connected to showing the Informer - let the screen reader calmly read it. - isInformerHidden - && ( - // If we are in a modal - always superfocus without concern for other elements - // on the page (user is unlikely to want to interact with the rest of the page) - isModal + isInformerHidden && + // If we are in a modal - always superfocus without concern for other elements + // on the page (user is unlikely to want to interact with the rest of the page) + (isModal || // If we are already inside of Uppy, or - || isFocusInUppy + isFocusInUppy || // If we are not focused on anything BUT we have already, at least once, focused on uppy // 1. We focus when isFocusNowhere, because when the element we were focused // on disappears (e.g. an overlay), - focus gets lost. If user is typing @@ -853,8 +1049,7 @@ export default class Dashboard extends UIPlugin { // [Practical check] Without '&& this.ifFocusedOnUppyRecently', in Safari, in inline mode, // when file is uploading, - navigate via tab to the checkbox, // try to press space multiple times. Focus will jump to Uppy. - || (isFocusNowhere && this.ifFocusedOnUppyRecently) - ) + (isFocusNowhere && this.ifFocusedOnUppyRecently)) ) { this.superFocus(this.el, this.getPluginState().activeOverlayType) } else { @@ -862,7 +1057,7 @@ export default class Dashboard extends UIPlugin { } } - afterUpdate = () => { + readonly afterUpdate = (): void => { if (this.opts.disabled && !this.dashboardIsDisabled) { this.disableInteractiveElements(true) return @@ -875,13 +1070,13 @@ export default class Dashboard extends UIPlugin { this.superFocusOnEachUpdate() } - saveFileCard = (meta, fileID) => { + saveFileCard = (meta: M, fileID: string): void => { this.uppy.setFileMeta(fileID, meta) this.toggleFileCard(false, fileID) } - #attachRenderFunctionToTarget = (target) => { - const plugin = this.uppy.getPlugin(target.id) + #attachRenderFunctionToTarget = (target: Target): TargetWithRender => { + const plugin = this.uppy.getPlugin(target.id) as any return { ...target, icon: plugin.icon || this.opts.defaultPickerIcon, @@ -889,8 +1084,10 @@ export default class Dashboard extends UIPlugin { } } - #isTargetSupported = (target) => { - const plugin = this.uppy.getPlugin(target.id) + #isTargetSupported = (target: Target) => { + const plugin = this.uppy.getPlugin(target.id) as any as { + isSupported?: () => boolean + } // If the plugin does not provide a `supported` check, assume the plugin works everywhere. if (typeof plugin.isSupported !== 'function') { return true @@ -898,25 +1095,28 @@ export default class Dashboard extends UIPlugin { return plugin.isSupported() } - #getAcquirers = memoize((targets) => { + #getAcquirers = memoize((targets: Target[]) => { return targets - .filter(target => target.type === 'acquirer' && this.#isTargetSupported(target)) + .filter( + (target) => + target.type === 'acquirer' && this.#isTargetSupported(target), + ) .map(this.#attachRenderFunctionToTarget) }) - #getProgressIndicators = memoize((targets) => { + #getProgressIndicators = memoize((targets: Target[]) => { return targets - .filter(target => target.type === 'progressindicator') + .filter((target) => target.type === 'progressindicator') .map(this.#attachRenderFunctionToTarget) }) - #getEditors = memoize((targets) => { + #getEditors = memoize((targets: Target[]) => { return targets - .filter(target => target.type === 'editor') + .filter((target) => target.type === 'editor') .map(this.#attachRenderFunctionToTarget) }) - render = (state) => { + render = (state: State): JSX.Element => { const pluginState = this.getPluginState() const { files, capabilities, allowNewUpload } = state const { @@ -945,10 +1145,15 @@ export default class Dashboard extends UIPlugin { theme = this.opts.theme } - if (['files', 'folders', 'both'].indexOf(this.opts.fileManagerSelectionType) < 0) { + if ( + ['files', 'folders', 'both'].indexOf(this.opts.fileManagerSelectionType) < + 0 + ) { this.opts.fileManagerSelectionType = 'files' // eslint-disable-next-line no-console - console.warn(`Unsupported option for "fileManagerSelectionType". Using default of "${this.opts.fileManagerSelectionType}".`) + console.warn( + `Unsupported option for "fileManagerSelectionType". Using default of "${this.opts.fileManagerSelectionType}".`, + ) } return DashboardUI({ @@ -1047,9 +1252,12 @@ export default class Dashboard extends UIPlugin { plugins.forEach((pluginID) => { const plugin = this.uppy.getPlugin(pluginID) if (plugin) { - plugin.mount(this, plugin) + ;(plugin as any).mount(this, plugin) } else { - this.uppy.log(`[Uppy] Dashboard could not find plugin '${pluginID}', make sure to uppy.use() the plugins you are specifying`, 'warning') + this.uppy.log( + `[Uppy] Dashboard could not find plugin '${pluginID}', make sure to uppy.use() the plugins you are specifying`, + 'warning', + ) } }) } @@ -1058,28 +1266,28 @@ export default class Dashboard extends UIPlugin { this.uppy.iteratePlugins(this.#addSupportedPluginIfNoTarget) } - #addSupportedPluginIfNoTarget = (plugin) => { + #addSupportedPluginIfNoTarget = (plugin?: UnknownPlugin) => { // Only these types belong on the Dashboard, // we wouldn’t want to try and mount Compressor or Tus, for example. const typesAllowed = ['acquirer', 'editor'] if (plugin && !plugin.opts?.target && typesAllowed.includes(plugin.type)) { const pluginAlreadyAdded = this.getPluginState().targets.some( - installedPlugin => plugin.id === installedPlugin.id, + (installedPlugin) => plugin.id === installedPlugin.id, ) if (!pluginAlreadyAdded) { - plugin.mount(this, plugin) + ;(plugin as any).mount(this, plugin) } } } - install = () => { + install = (): void => { // Set default state for Dashboard this.setPluginState({ isHidden: true, fileCardFor: null, activeOverlayType: null, showAddFilesPanel: false, - activePickerPanel: false, + activePickerPanel: undefined, showFileEditor: false, metaFields: this.opts.metaFields, targets: [], @@ -1090,12 +1298,20 @@ export default class Dashboard extends UIPlugin { const { inline, closeAfterFinish } = this.opts if (inline && closeAfterFinish) { - throw new Error('[Dashboard] `closeAfterFinish: true` cannot be used on an inline Dashboard, because an inline Dashboard cannot be closed at all. Either set `inline: false`, or disable the `closeAfterFinish` option.') + throw new Error( + '[Dashboard] `closeAfterFinish: true` cannot be used on an inline Dashboard, because an inline Dashboard cannot be closed at all. Either set `inline: false`, or disable the `closeAfterFinish` option.', + ) } const { allowMultipleUploads, allowMultipleUploadBatches } = this.uppy.opts - if ((allowMultipleUploads || allowMultipleUploadBatches) && closeAfterFinish) { - this.uppy.log('[Dashboard] When using `closeAfterFinish`, we recommended setting the `allowMultipleUploadBatches` option to `false` in the Uppy constructor. See https://uppy.io/docs/uppy/#allowMultipleUploads-true', 'warning') + if ( + (allowMultipleUploads || allowMultipleUploadBatches) && + closeAfterFinish + ) { + this.uppy.log( + '[Dashboard] When using `closeAfterFinish`, we recommended setting the `allowMultipleUploadBatches` option to `false` in the Uppy constructor. See https://uppy.io/docs/uppy/#allowMultipleUploads-true', + 'warning', + ) } const { target } = this.opts @@ -1139,16 +1355,20 @@ export default class Dashboard extends UIPlugin { } // Dark Mode / theme - this.darkModeMediaQuery = (typeof window !== 'undefined' && window.matchMedia) - ? window.matchMedia('(prefers-color-scheme: dark)') + this.darkModeMediaQuery = + typeof window !== 'undefined' && window.matchMedia ? + window.matchMedia('(prefers-color-scheme: dark)') : null - const isDarkModeOnFromTheStart = this.darkModeMediaQuery ? this.darkModeMediaQuery.matches : false - this.uppy.log(`[Dashboard] Dark mode is ${isDarkModeOnFromTheStart ? 'on' : 'off'}`) + const isDarkModeOnFromTheStart = + this.darkModeMediaQuery ? this.darkModeMediaQuery.matches : false + this.uppy.log( + `[Dashboard] Dark mode is ${isDarkModeOnFromTheStart ? 'on' : 'off'}`, + ) this.setDarkModeCapability(isDarkModeOnFromTheStart) if (this.opts.theme === 'auto') { - this.darkModeMediaQuery.addListener(this.handleSystemDarkModeChange) + this.darkModeMediaQuery?.addListener(this.handleSystemDarkModeChange) } this.#addSpecifiedPluginsFromOptions() @@ -1156,7 +1376,7 @@ export default class Dashboard extends UIPlugin { this.initEvents() } - uninstall = () => { + uninstall = (): void => { if (!this.opts.disableInformer) { const informer = this.uppy.getPlugin(`${this.id}:Informer`) // Checking if this plugin exists, in case it was removed by uppy-core @@ -1177,11 +1397,11 @@ export default class Dashboard extends UIPlugin { const plugins = this.opts.plugins || [] plugins.forEach((pluginID) => { const plugin = this.uppy.getPlugin(pluginID) - if (plugin) plugin.unmount() + if (plugin) (plugin as any).unmount() }) if (this.opts.theme === 'auto') { - this.darkModeMediaQuery.removeListener(this.handleSystemDarkModeChange) + this.darkModeMediaQuery?.removeListener(this.handleSystemDarkModeChange) } if (this.opts.disablePageScrollWhenModalOpen) { diff --git a/packages/@uppy/dashboard/src/components/AddFiles.jsx b/packages/@uppy/dashboard/src/components/AddFiles.jsx deleted file mode 100644 index c9a547a2dc..0000000000 --- a/packages/@uppy/dashboard/src/components/AddFiles.jsx +++ /dev/null @@ -1,325 +0,0 @@ -import { h, Component, Fragment } from 'preact' - -class AddFiles extends Component { - triggerFileInputClick = () => { - this.fileInput.click() - } - - triggerFolderInputClick = () => { - this.folderInput.click() - } - - triggerVideoCameraInputClick = () => { - this.mobileVideoFileInput.click() - } - - triggerPhotoCameraInputClick = () => { - this.mobilePhotoFileInput.click() - } - - onFileInputChange = (event) => { - this.props.handleInputChange(event) - - // We clear the input after a file is selected, because otherwise - // change event is not fired in Chrome and Safari when a file - // with the same name is selected. - // ___Why not use value="" on instead? - // Because if we use that method of clearing the input, - // Chrome will not trigger change if we drop the same file twice (Issue #768). - event.target.value = null // eslint-disable-line no-param-reassign - } - - renderHiddenInput = (isFolder, refCallback) => { - return ( - - ) - } - - renderHiddenCameraInput = (type, nativeCameraFacingMode, refCallback) => { - const typeToAccept = { photo: 'image/*', video: 'video/*' } - const accept = typeToAccept[type] - - return ( - - ) - } - - renderMyDeviceAcquirer = () => { - return ( -
- -
- ) - } - - renderPhotoCamera = () => { - return ( -
- -
- ) - } - - renderVideoCamera = () => { - return ( -
- -
- ) - } - - renderBrowseButton = (text, onClickFn) => { - const numberOfAcquirers = this.props.acquirers.length - return ( - - ) - } - - renderDropPasteBrowseTagline = (numberOfAcquirers) => { - 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 ( -
- { - // eslint-disable-next-line no-nested-ternary - 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 }) - } -
- ) - } - - [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') - } - - renderAcquirer = (acquirer) => { - return ( -
- -
- ) - } - - renderAcquirers = (acquirers) => { - // 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))} - - - ) - } - - renderSourcesList = (acquirers, disableLocalFiles) => { - const { showNativePhotoCameraButton, showNativeVideoCameraButton } = this.props - - let list = [] - - 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) => ({ 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) - - const renderList = (l) => l.map(({ key, elements }) => {elements}) - - return ( - <> - {this.renderDropPasteBrowseTagline(list.length)} - -
- {renderList(listWithoutLastTwo)} - - - {renderList(lastTwo)} - -
- - ) - } - - renderPoweredByUppy () { - const { i18nArray } = this.props - - const uppyBranding = ( - - - Uppy - - ) - - const linkText = i18nArray('poweredBy', { uppy: uppyBranding }) - - return ( - - {linkText} - - ) - } - - render () { - 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(this.props)} -
-
- ) - } -} - -export default AddFiles diff --git a/packages/@uppy/dashboard/src/components/AddFiles.tsx b/packages/@uppy/dashboard/src/components/AddFiles.tsx new file mode 100644 index 0000000000..da2d93d5e9 --- /dev/null +++ b/packages/@uppy/dashboard/src/components/AddFiles.tsx @@ -0,0 +1,450 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck Typing this file requires more work, skipping it to unblock the rest of the transition. + +/* eslint-disable react/destructuring-assignment */ +import { h, Component, Fragment, type ComponentChild } from 'preact' + +type $TSFixMe = any + +class AddFiles extends Component { + fileInput: $TSFixMe + + folderInput: $TSFixMe + + mobilePhotoFileInput: $TSFixMe + + mobileVideoFileInput: $TSFixMe + + private triggerFileInputClick = () => { + this.fileInput.click() + } + + private triggerFolderInputClick = () => { + this.folderInput.click() + } + + private triggerVideoCameraInputClick = () => { + this.mobileVideoFileInput.click() + } + + private triggerPhotoCameraInputClick = () => { + this.mobilePhotoFileInput.click() + } + + private onFileInputChange = (event: $TSFixMe) => { + this.props.handleInputChange(event) + + // We clear the input after a file is selected, because otherwise + // change event is not fired in Chrome and Safari when a file + // with the same name is selected. + // ___Why not use value="" on instead? + // Because if we use that method of clearing the input, + // Chrome will not trigger change if we drop the same file twice (Issue #768). + event.target.value = null // eslint-disable-line no-param-reassign + } + + private renderHiddenInput = (isFolder: $TSFixMe, refCallback: $TSFixMe) => { + return ( + + ) + } + + private renderHiddenCameraInput = ( + type: $TSFixMe, + nativeCameraFacingMode: $TSFixMe, + refCallback: $TSFixMe, + ) => { + const typeToAccept = { photo: 'image/*', video: 'video/*' } + const accept = typeToAccept[type] + + return ( + + ) + } + + private renderMyDeviceAcquirer = () => { + return ( +
+ +
+ ) + } + + private renderPhotoCamera = () => { + return ( +
+ +
+ ) + } + + private renderVideoCamera = () => { + return ( +
+ +
+ ) + } + + private renderBrowseButton = (text: $TSFixMe, onClickFn: $TSFixMe) => { + const numberOfAcquirers = this.props.acquirers.length + return ( + + ) + } + + private renderDropPasteBrowseTagline = (numberOfAcquirers: $TSFixMe) => { + 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 ( +
+ { + // eslint-disable-next-line no-nested-ternary + 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: $TSFixMe) => { + return ( +
+ +
+ ) + } + + private renderAcquirers = (acquirers: $TSFixMe) => { + // 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: $TSFixMe, + disableLocalFiles: $TSFixMe, + ) => { + const { showNativePhotoCameraButton, showNativeVideoCameraButton } = + this.props + + let list = [] + + 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: $TSFixMe) => ({ + 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) + + const renderList = (l: $TSFixMe) => + l.map(({ key, elements }: $TSFixMe) => ( + {elements} + )) + + return ( + <> + {this.renderDropPasteBrowseTagline(list.length)} + +
+ {renderList(listWithoutLastTwo)} + + + {renderList(lastTwo)} + +
+ + ) + } + + private renderPoweredByUppy() { + const { i18nArray } = this.props as $TSFixMe + + const uppyBranding = ( + + + Uppy + + ) + + const linkText = i18nArray('poweredBy', { uppy: uppyBranding }) + + return ( + + {linkText} + + ) + } + + render(): ComponentChild { + const { + showNativePhotoCameraButton, + showNativeVideoCameraButton, + nativeCameraFacingMode, + } = this.props + + return ( +
+ {this.renderHiddenInput(false, (ref: $TSFixMe) => { + this.fileInput = ref + })} + {this.renderHiddenInput(true, (ref: $TSFixMe) => { + this.folderInput = ref + })} + {showNativePhotoCameraButton && + this.renderHiddenCameraInput( + 'photo', + nativeCameraFacingMode, + (ref: $TSFixMe) => { + this.mobilePhotoFileInput = ref + }, + )} + {showNativeVideoCameraButton && + this.renderHiddenCameraInput( + 'video', + nativeCameraFacingMode, + (ref: $TSFixMe) => { + this.mobileVideoFileInput = ref + }, + )} + {this.renderSourcesList( + this.props.acquirers, + this.props.disableLocalFiles, + )} +
+ {this.props.note && ( +
{this.props.note}
+ )} + {this.props.proudlyDisplayPoweredByUppy && + this.renderPoweredByUppy(this.props)} +
+
+ ) + } +} + +export default AddFiles diff --git a/packages/@uppy/dashboard/src/components/AddFilesPanel.jsx b/packages/@uppy/dashboard/src/components/AddFilesPanel.tsx similarity index 71% rename from packages/@uppy/dashboard/src/components/AddFilesPanel.jsx rename to packages/@uppy/dashboard/src/components/AddFilesPanel.tsx index 1dbf032754..f0faea74af 100644 --- a/packages/@uppy/dashboard/src/components/AddFilesPanel.jsx +++ b/packages/@uppy/dashboard/src/components/AddFilesPanel.tsx @@ -1,8 +1,11 @@ +/* eslint-disable react/destructuring-assignment */ import { h } from 'preact' import classNames from 'classnames' -import AddFiles from './AddFiles.jsx' +import AddFiles from './AddFiles.tsx' -const AddFilesPanel = (props) => { +type $TSFixMe = any + +const AddFilesPanel = (props: $TSFixMe): $TSFixMe => { return (
{ aria-hidden={!props.showAddFilesPanel} >
-
+
{props.i18n('addingMoreFiles')}
- ) : null} + : null}
@@ -126,9 +140,19 @@ export default function Dashboard (props) { {numberOfFilesForRecovery && (
-
)} - {showFileList ? ( - - ) : ( - // eslint-disable-next-line react/jsx-props-no-spreading - - )} + { + showFileList ? + + // eslint-disable-next-line react/jsx-props-no-spreading + : + } {/* eslint-disable-next-line react/jsx-props-no-spreading */} - {props.showAddFilesPanel ? : null} + {props.showAddFilesPanel ? + + : null} {/* eslint-disable-next-line react/jsx-props-no-spreading */} - {props.fileCardFor ? : null} + {props.fileCardFor ? + + : null} {/* eslint-disable-next-line react/jsx-props-no-spreading */} - {props.activePickerPanel ? : null} + {props.activePickerPanel ? + + : null} {/* eslint-disable-next-line react/jsx-props-no-spreading */} - {props.showFileEditor ? : null} + {props.showFileEditor ? + + : null}
- {props.progressindicators.map((target) => { + {props.progressindicators.map((target: $TSFixMe) => { return props.uppy.getPlugin(target.id).render(props.state) })}
diff --git a/packages/@uppy/dashboard/src/components/EditorPanel.jsx b/packages/@uppy/dashboard/src/components/EditorPanel.tsx similarity index 70% rename from packages/@uppy/dashboard/src/components/EditorPanel.jsx rename to packages/@uppy/dashboard/src/components/EditorPanel.tsx index dae46e19e8..59c3f1ffdc 100644 --- a/packages/@uppy/dashboard/src/components/EditorPanel.jsx +++ b/packages/@uppy/dashboard/src/components/EditorPanel.tsx @@ -1,7 +1,10 @@ +/* eslint-disable react/destructuring-assignment */ import { h } from 'preact' import classNames from 'classnames' -function EditorPanel (props) { +type $TSFixMe = any + +function EditorPanel(props: $TSFixMe): JSX.Element { const file = props.files[props.fileCardFor] const handleCancel = () => { @@ -17,9 +20,17 @@ function EditorPanel (props) { id="uppy-DashboardContent-panel--editor" >
-
+
{props.i18nArray('editing', { - file: {file.meta ? file.meta.name : file.name}, + file: ( + + {file.meta ? file.meta.name : file.name} + + ), })}
- {props.editors.map((target) => { + {props.editors.map((target: $TSFixMe) => { return props.uppy.getPlugin(target.id).render(props.state) })}
diff --git a/packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.jsx b/packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.jsx deleted file mode 100644 index 97984e5d8a..0000000000 --- a/packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import { h } from 'preact' - -export default function RenderMetaFields (props) { - const { - computedMetaFields, - requiredMetaFields, - updateMeta, - form, - formState, - } = props - - const fieldCSSClasses = { - text: 'uppy-u-reset uppy-c-textInput uppy-Dashboard-FileCard-input', - } - - return computedMetaFields.map((field) => { - const id = `uppy-Dashboard-FileCard-input-${field.id}` - const required = requiredMetaFields.includes(field.id) - return ( -
- - {field.render !== undefined - ? field.render({ - value: formState[field.id], - onChange: (newVal) => updateMeta(newVal, field.id), - fieldCSSClasses, - required, - form: form.id, - }, h) - : ( - updateMeta(ev.target.value, field.id)} - data-uppy-super-focusable - /> - )} -
- ) - }) -} diff --git a/packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.tsx b/packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.tsx new file mode 100644 index 0000000000..0c8f942fa9 --- /dev/null +++ b/packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.tsx @@ -0,0 +1,54 @@ +import { h } from 'preact' + +type $TSFixMe = any + +export default function RenderMetaFields(props: $TSFixMe): JSX.Element { + const { + computedMetaFields, + requiredMetaFields, + updateMeta, + form, + formState, + } = props + + const fieldCSSClasses = { + text: 'uppy-u-reset uppy-c-textInput uppy-Dashboard-FileCard-input', + } + + return computedMetaFields.map((field: $TSFixMe) => { + const id = `uppy-Dashboard-FileCard-input-${field.id}` + const required = requiredMetaFields.includes(field.id) + return ( +
+ + {field.render !== undefined ? + field.render( + { + value: formState[field.id], + onChange: (newVal: $TSFixMe) => updateMeta(newVal, field.id), + fieldCSSClasses, + required, + form: form.id, + }, + h, + ) + : + updateMeta((ev.target as HTMLInputElement).value, field.id) + } + data-uppy-super-focusable + /> + } +
+ ) + }) +} diff --git a/packages/@uppy/dashboard/src/components/FileCard/index.jsx b/packages/@uppy/dashboard/src/components/FileCard/index.tsx similarity index 75% rename from packages/@uppy/dashboard/src/components/FileCard/index.jsx rename to packages/@uppy/dashboard/src/components/FileCard/index.tsx index e334f50b41..d7d8c59bbd 100644 --- a/packages/@uppy/dashboard/src/components/FileCard/index.jsx +++ b/packages/@uppy/dashboard/src/components/FileCard/index.tsx @@ -2,12 +2,14 @@ import { h } from 'preact' import { useEffect, useState, useCallback } from 'preact/hooks' import classNames from 'classnames' import { nanoid } from 'nanoid/non-secure' -import getFileTypeIcon from '../../utils/getFileTypeIcon.jsx' -import ignoreEvent from '../../utils/ignoreEvent.js' -import FilePreview from '../FilePreview.jsx' -import RenderMetaFields from './RenderMetaFields.jsx' +import getFileTypeIcon from '../../utils/getFileTypeIcon.tsx' +import ignoreEvent from '../../utils/ignoreEvent.ts' +import FilePreview from '../FilePreview.tsx' +import RenderMetaFields from './RenderMetaFields.tsx' -export default function FileCard (props) { +type $TSFixMe = any + +export default function FileCard(props: $TSFixMe): JSX.Element { const { files, fileCardFor, @@ -23,8 +25,8 @@ export default function FileCard (props) { } = props const getMetaFields = () => { - return typeof metaFields === 'function' - ? metaFields(files[fileCardFor]) + return typeof metaFields === 'function' ? + metaFields(files[fileCardFor]) : metaFields } @@ -32,19 +34,22 @@ export default function FileCard (props) { const computedMetaFields = getMetaFields() ?? [] const showEditButton = canEditFile(file) - const storedMetaData = {} - computedMetaFields.forEach((field) => { + const storedMetaData: Record = {} + computedMetaFields.forEach((field: $TSFixMe) => { storedMetaData[field.id] = file.meta[field.id] ?? '' }) const [formState, setFormState] = useState(storedMetaData) - const handleSave = useCallback((ev) => { - ev.preventDefault() - saveFileCard(formState, fileCardFor) - }, [saveFileCard, formState, fileCardFor]) + const handleSave = useCallback( + (ev: $TSFixMe) => { + ev.preventDefault() + saveFileCard(formState, fileCardFor) + }, + [saveFileCard, formState, fileCardFor], + ) - const updateMeta = (newVal, name) => { + const updateMeta = (newVal: $TSFixMe, name: $TSFixMe) => { setFormState({ ...formState, [name]: newVal, @@ -81,9 +86,17 @@ export default function FileCard (props) { onPaste={ignoreEvent} >
-
+
{i18nArray('editing', { - file: {file.meta ? file.meta.name : file.name}, + file: ( + + {file.meta ? file.meta.name : file.name} + + ), })}
- )} + )}
diff --git a/packages/@uppy/dashboard/src/components/FileItem/Buttons/index.jsx b/packages/@uppy/dashboard/src/components/FileItem/Buttons/index.tsx similarity index 63% rename from packages/@uppy/dashboard/src/components/FileItem/Buttons/index.jsx rename to packages/@uppy/dashboard/src/components/FileItem/Buttons/index.tsx index 0f23814d8e..8d6f288e6c 100644 --- a/packages/@uppy/dashboard/src/components/FileItem/Buttons/index.jsx +++ b/packages/@uppy/dashboard/src/components/FileItem/Buttons/index.tsx @@ -1,17 +1,19 @@ -import { h } from 'preact' -import copyToClipboard from '../../../utils/copyToClipboard.js' +import { h, type ComponentChild } from 'preact' +import copyToClipboard from '../../../utils/copyToClipboard.ts' -function EditButton ({ +type $TSFixMe = any + +function EditButton({ file, uploadInProgressOrComplete, metaFields, canEditFile, i18n, onClick, -}) { +}: $TSFixMe) { if ( - (!uploadInProgressOrComplete && metaFields && metaFields.length > 0) - || (!uploadInProgressOrComplete && canEditFile(file)) + (!uploadInProgressOrComplete && metaFields && metaFields.length > 0) || + (!uploadInProgressOrComplete && canEditFile(file)) ) { return ( @@ -34,7 +49,7 @@ function EditButton ({ return null } -function RemoveButton ({ i18n, onClick, file }) { +function RemoveButton({ i18n, onClick, file }: $TSFixMe) { return ( ) } -const copyLinkToClipboard = (event, props) => { - copyToClipboard(props.file.uploadURL, props.i18n('copyLinkToClipboardFallback')) +const copyLinkToClipboard = (event: $TSFixMe, props: $TSFixMe) => { + copyToClipboard( + props.file.uploadURL, + props.i18n('copyLinkToClipboardFallback'), + ) .then(() => { props.uppy.log('Link copied to clipboard.') props.uppy.info(props.i18n('copyLinkToClipboardSuccess'), 'info', 3000) @@ -62,7 +90,7 @@ const copyLinkToClipboard = (event, props) => { .then(() => event.target.focus({ preventScroll: true })) } -function CopyLinkButton (props) { +function CopyLinkButton(props: $TSFixMe) { const { i18n } = props return ( @@ -73,14 +101,21 @@ function CopyLinkButton (props) { title={i18n('copyLink')} onClick={(event) => copyLinkToClipboard(event, props)} > -
) } diff --git a/packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.jsx b/packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.tsx similarity index 70% rename from packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.jsx rename to packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.tsx index 3d32b3c896..f721b59572 100644 --- a/packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.jsx +++ b/packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.tsx @@ -1,12 +1,15 @@ -import { h, Fragment } from 'preact' +/* eslint-disable react/destructuring-assignment */ +import { h, Fragment, type ComponentChild } from 'preact' import prettierBytes from '@transloadit/prettier-bytes' import truncateString from '@uppy/utils/lib/truncateString' -import MetaErrorMessage from '../MetaErrorMessage.jsx' +import MetaErrorMessage from '../MetaErrorMessage.tsx' -const renderFileName = (props) => { +type $TSFixMe = any + +const renderFileName = (props: $TSFixMe) => { const { author, name } = props.file.meta - function getMaxNameLength () { + function getMaxNameLength() { if (props.isSingleFile && props.containerHeight >= 350) { return 90 } @@ -29,7 +32,7 @@ const renderFileName = (props) => { ) } -const renderAuthor = (props) => { +const renderAuthor = (props: $TSFixMe) => { const { author } = props.file.meta const providerName = props.file.remote?.providerName const dot = `\u00B7` @@ -47,37 +50,39 @@ const renderAuthor = (props) => { > {truncateString(author.name, 13)} - {providerName ? ( + {providerName ? <> {` ${dot} `} {providerName} {` ${dot} `} - ) : null} + : null}
) } -const renderFileSize = (props) => props.file.size && ( -
- {prettierBytes(props.file.size)} -
-) +const renderFileSize = (props: $TSFixMe) => + props.file.size && ( +
+ {prettierBytes(props.file.size)} +
+ ) -const ReSelectButton = (props) => props.file.isGhost && ( - - {' \u2022 '} - - -) +const ReSelectButton = (props: $TSFixMe) => + props.file.isGhost && ( + + {' \u2022 '} + + + ) -const ErrorButton = ({ file, onClick }) => { +const ErrorButton = ({ file, onClick }: $TSFixMe) => { if (file.error) { return (
@@ -125,7 +133,14 @@ export default function FileProgress (props) { return ( // eslint-disable-next-line react/jsx-props-no-spreading -
{i18n('missingRequiredMetaFields', { smart_count: missingRequiredMetaFields.length, fields: metaFieldsString, - })} - {' '} + })}{' '} - ) : ( -
- )} + :
} -
+
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
- {allowNewUpload ? ( + {allowNewUpload ? - ) : ( -
- )} + :
}
) } diff --git a/packages/@uppy/dashboard/src/components/Slide.jsx b/packages/@uppy/dashboard/src/components/Slide.jsx deleted file mode 100644 index b28065a255..0000000000 --- a/packages/@uppy/dashboard/src/components/Slide.jsx +++ /dev/null @@ -1,86 +0,0 @@ -import { cloneElement, toChildArray } from 'preact' -import { useEffect, useState, useRef } from 'preact/hooks' -import classNames from 'classnames' - -const transitionName = 'uppy-transition-slideDownUp' -const duration = 250 - -/** - * Vertical slide transition. - * - * This can take a _single_ child component, which _must_ accept a `className` prop. - * - * Currently this is specific to the `uppy-transition-slideDownUp` transition, - * but it should be simple to extend this for any type of single-element - * transition by setting the CSS name and duration as props. - */ -function Slide ({ children }) { - const [cachedChildren, setCachedChildren] = useState(null); - const [className, setClassName] = useState(''); - const enterTimeoutRef = useRef(); - const leaveTimeoutRef = useRef(); - const animationFrameRef = useRef(); - - const handleEnterTransition = () => { - setClassName(`${transitionName}-enter`); - - cancelAnimationFrame(animationFrameRef.current); - clearTimeout(leaveTimeoutRef.current); - leaveTimeoutRef.current = undefined; - - animationFrameRef.current = requestAnimationFrame(() => { - setClassName(`${transitionName}-enter ${transitionName}-enter-active`); - - enterTimeoutRef.current = setTimeout(() => { - setClassName(''); - }, duration); - }); - }; - - const handleLeaveTransition = () => { - setClassName(`${transitionName}-leave`); - - cancelAnimationFrame(animationFrameRef.current); - clearTimeout(enterTimeoutRef.current); - enterTimeoutRef.current = undefined; - - animationFrameRef.current = requestAnimationFrame(() => { - setClassName(`${transitionName}-leave ${transitionName}-leave-active`); - - leaveTimeoutRef.current = setTimeout(() => { - setCachedChildren(null); - setClassName(''); - }, duration); - }); - }; - - useEffect(() => { - const child = toChildArray(children)[0]; - if (cachedChildren === child) return; - - if (child && !cachedChildren) { - handleEnterTransition(); - } else if (cachedChildren && !child && !leaveTimeoutRef.current) { - handleLeaveTransition(); - } - - setCachedChildren(child); - }, [children, cachedChildren]); // Dependency array to trigger effect on children change - - - useEffect(() => { - return () => { - clearTimeout(enterTimeoutRef.current); - clearTimeout(leaveTimeoutRef.current); - cancelAnimationFrame(animationFrameRef.current); - }; - }, []); // Cleanup useEffect - - if (!cachedChildren) return null; - - return cloneElement(cachedChildren, { - className: classNames(className, cachedChildren.props.className), - }); -}; - -export default Slide diff --git a/packages/@uppy/dashboard/src/components/Slide.tsx b/packages/@uppy/dashboard/src/components/Slide.tsx new file mode 100644 index 0000000000..cca46963bc --- /dev/null +++ b/packages/@uppy/dashboard/src/components/Slide.tsx @@ -0,0 +1,96 @@ +import { + cloneElement, + toChildArray, + type VNode, + type ComponentChildren, +} from 'preact' +import { useEffect, useState, useRef } from 'preact/hooks' +import classNames from 'classnames' + +const transitionName = 'uppy-transition-slideDownUp' +const duration = 250 + +/** + * Vertical slide transition. + * + * This can take a _single_ child component, which _must_ accept a `className` prop. + * + * Currently this is specific to the `uppy-transition-slideDownUp` transition, + * but it should be simple to extend this for any type of single-element + * transition by setting the CSS name and duration as props. + */ +function Slide({ + children, +}: { + children: ComponentChildren +}): JSX.Element | null { + const [cachedChildren, setCachedChildren] = useState | null>(null) + const [className, setClassName] = useState('') + const enterTimeoutRef = useRef>() + const leaveTimeoutRef = useRef>() + const animationFrameRef = useRef>() + + const handleEnterTransition = () => { + setClassName(`${transitionName}-enter`) + + cancelAnimationFrame(animationFrameRef.current!) + clearTimeout(leaveTimeoutRef.current) + leaveTimeoutRef.current = undefined + + animationFrameRef.current = requestAnimationFrame(() => { + setClassName(`${transitionName}-enter ${transitionName}-enter-active`) + + enterTimeoutRef.current = setTimeout(() => { + setClassName('') + }, duration) + }) + } + + const handleLeaveTransition = () => { + setClassName(`${transitionName}-leave`) + + cancelAnimationFrame(animationFrameRef.current!) + clearTimeout(enterTimeoutRef.current) + enterTimeoutRef.current = undefined + + animationFrameRef.current = requestAnimationFrame(() => { + setClassName(`${transitionName}-leave ${transitionName}-leave-active`) + + leaveTimeoutRef.current = setTimeout(() => { + setCachedChildren(null) + setClassName('') + }, duration) + }) + } + + useEffect(() => { + const child = toChildArray(children)[0] as VNode + if (cachedChildren === child) return + + if (child && !cachedChildren) { + handleEnterTransition() + } else if (cachedChildren && !child && !leaveTimeoutRef.current) { + handleLeaveTransition() + } + + setCachedChildren(child) + }, [children, cachedChildren]) // Dependency array to trigger effect on children change + + useEffect(() => { + return () => { + clearTimeout(enterTimeoutRef.current) + clearTimeout(leaveTimeoutRef.current) + cancelAnimationFrame(animationFrameRef.current!) + } + }, []) // Cleanup useEffect + + if (!cachedChildren) return null + + return cloneElement(cachedChildren, { + className: classNames(className, cachedChildren.props.className), + }) +} + +export default Slide diff --git a/packages/@uppy/dashboard/src/index.js b/packages/@uppy/dashboard/src/index.js deleted file mode 100644 index 6c74a32596..0000000000 --- a/packages/@uppy/dashboard/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Dashboard.jsx' diff --git a/packages/@uppy/dashboard/src/index.test.js b/packages/@uppy/dashboard/src/index.test.ts similarity index 77% rename from packages/@uppy/dashboard/src/index.test.js rename to packages/@uppy/dashboard/src/index.test.ts index 862c3ab7db..d584cb927a 100644 --- a/packages/@uppy/dashboard/src/index.test.js +++ b/packages/@uppy/dashboard/src/index.test.ts @@ -1,19 +1,29 @@ import { afterAll, beforeAll, describe, it, expect } from 'vitest' -import Core from '@uppy/core' +import Core, { UIPlugin } from '@uppy/core' import StatusBarPlugin from '@uppy/status-bar' import GoogleDrivePlugin from '@uppy/google-drive' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore untyped import WebcamPlugin from '@uppy/webcam' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore untyped import Url from '@uppy/url' import resizeObserverPolyfill from 'resize-observer-polyfill' -import DashboardPlugin from '../lib/index.js' +import DashboardPlugin from './index.ts' + +type $TSFixMe = any describe('Dashboard', () => { beforeAll(() => { - globalThis.ResizeObserver = resizeObserverPolyfill.default || resizeObserverPolyfill + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore we're touching globals for the test + globalThis.ResizeObserver = + (resizeObserverPolyfill as any).default || resizeObserverPolyfill }) afterAll(() => { + // @ts-expect-error we're touching globals for the test delete globalThis.ResizeObserver }) @@ -48,7 +58,10 @@ describe('Dashboard', () => { inline: true, target: 'body', }) - core.use(GoogleDrivePlugin, { target: DashboardPlugin, companionUrl: 'https://fake.uppy.io/' }) + core.use(GoogleDrivePlugin, { + target: DashboardPlugin as $TSFixMe, + companionUrl: 'https://fake.uppy.io/', + }) }).not.toThrow() core.close() @@ -75,12 +88,15 @@ describe('Dashboard', () => { core.use(DashboardPlugin, { inline: false }) core.use(WebcamPlugin) - const dashboardPlugins = core.getState().plugins['Dashboard'].targets + const dashboardPlugins = core.getState().plugins['Dashboard']! + .targets as UIPlugin[] // two built-in plugins + these ones below expect(dashboardPlugins.length).toEqual(4) expect(dashboardPlugins.some((plugin) => plugin.id === 'Url')).toEqual(true) - expect(dashboardPlugins.some((plugin) => plugin.id === 'Webcam')).toEqual(true) + expect(dashboardPlugins.some((plugin) => plugin.id === 'Webcam')).toEqual( + true, + ) core.close() }) @@ -92,12 +108,15 @@ describe('Dashboard', () => { core.use(DashboardPlugin, { inline: false }) core.use(WebcamPlugin, { target: 'body' }) - const dashboardPlugins = core.getState().plugins['Dashboard'].targets + const dashboardPlugins = core.getState().plugins['Dashboard']! + .targets as UIPlugin[] // two built-in plugins + these ones below expect(dashboardPlugins.length).toEqual(3) expect(dashboardPlugins.some((plugin) => plugin.id === 'Url')).toEqual(true) - expect(dashboardPlugins.some((plugin) => plugin.id === 'Webcam')).toEqual(false) + expect(dashboardPlugins.some((plugin) => plugin.id === 'Webcam')).toEqual( + false, + ) core.close() }) @@ -109,13 +128,11 @@ describe('Dashboard', () => { target: 'body', }) - core.getPlugin('Dashboard').setOptions({ + core.getPlugin('Dashboard')!.setOptions({ width: 300, }) - expect( - core.getPlugin('Dashboard').opts.width, - ).toEqual(300) + expect(core.getPlugin('Dashboard')!.opts.width).toEqual(300) }) it('should use updated locale from Core, when it’s set via Core’s setOptions()', () => { @@ -133,16 +150,14 @@ describe('Dashboard', () => { }, }) - expect( - core.getPlugin('Dashboard').i18n('myDevice'), - ).toEqual('Май дивайс') + expect(core.getPlugin('Dashboard')!.i18n('myDevice')).toEqual('Май дивайс') }) it('should accept a callback as `metaFields` option', () => { const core = new Core() expect(() => { core.use(DashboardPlugin, { - metaFields: (file) => { + metaFields: (file: any) => { const fields = [{ id: 'name', name: 'File name' }] if (file.type.startsWith('image/')) { fields.push({ id: 'location', name: 'Photo Location' }) diff --git a/packages/@uppy/dashboard/src/index.ts b/packages/@uppy/dashboard/src/index.ts new file mode 100644 index 0000000000..9355137dc9 --- /dev/null +++ b/packages/@uppy/dashboard/src/index.ts @@ -0,0 +1 @@ +export { default } from './Dashboard.tsx' diff --git a/packages/@uppy/dashboard/src/locale.js b/packages/@uppy/dashboard/src/locale.ts similarity index 100% rename from packages/@uppy/dashboard/src/locale.js rename to packages/@uppy/dashboard/src/locale.ts diff --git a/packages/@uppy/dashboard/src/utils/copyToClipboard.test.js b/packages/@uppy/dashboard/src/utils/copyToClipboard.test.ts similarity index 80% rename from packages/@uppy/dashboard/src/utils/copyToClipboard.test.js rename to packages/@uppy/dashboard/src/utils/copyToClipboard.test.ts index 328ee49588..df8e9d9a5e 100644 --- a/packages/@uppy/dashboard/src/utils/copyToClipboard.test.js +++ b/packages/@uppy/dashboard/src/utils/copyToClipboard.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import copyToClipboard from './copyToClipboard.js' +import copyToClipboard from './copyToClipboard.ts' describe('copyToClipboard', () => { it.skip('should copy the specified text to the clipboard', () => { diff --git a/packages/@uppy/dashboard/src/utils/copyToClipboard.js b/packages/@uppy/dashboard/src/utils/copyToClipboard.ts similarity index 82% rename from packages/@uppy/dashboard/src/utils/copyToClipboard.js rename to packages/@uppy/dashboard/src/utils/copyToClipboard.ts index 6f738730bc..6720cec9b7 100644 --- a/packages/@uppy/dashboard/src/utils/copyToClipboard.js +++ b/packages/@uppy/dashboard/src/utils/copyToClipboard.ts @@ -8,8 +8,14 @@ * @param {string} fallbackString * @returns {Promise} */ -export default function copyToClipboard (textToCopy, fallbackString = 'Copy the URL below') { - return new Promise((resolve) => { + +type $TSFixMe = any + +export default function copyToClipboard( + textToCopy: $TSFixMe, + fallbackString = 'Copy the URL below', +): $TSFixMe { + return new Promise((resolve) => { const textArea = document.createElement('textarea') textArea.setAttribute('style', { position: 'fixed', @@ -22,13 +28,13 @@ export default function copyToClipboard (textToCopy, fallbackString = 'Copy the outline: 'none', boxShadow: 'none', background: 'transparent', - }) + } as $TSFixMe as string) textArea.value = textToCopy document.body.appendChild(textArea) textArea.select() - const magicCopyFailed = () => { + const magicCopyFailed = (cause?: unknown) => { document.body.removeChild(textArea) // eslint-disable-next-line no-alert window.prompt(fallbackString, textToCopy) diff --git a/packages/@uppy/dashboard/src/utils/createSuperFocus.test.js b/packages/@uppy/dashboard/src/utils/createSuperFocus.test.ts similarity index 86% rename from packages/@uppy/dashboard/src/utils/createSuperFocus.test.js rename to packages/@uppy/dashboard/src/utils/createSuperFocus.test.ts index f30f1bc22d..1230dbf471 100644 --- a/packages/@uppy/dashboard/src/utils/createSuperFocus.test.js +++ b/packages/@uppy/dashboard/src/utils/createSuperFocus.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import createSuperFocus from './createSuperFocus.js' +import createSuperFocus from './createSuperFocus.ts' describe('createSuperFocus', () => { // superFocus.cancel() is used in dashboard diff --git a/packages/@uppy/dashboard/src/utils/createSuperFocus.js b/packages/@uppy/dashboard/src/utils/createSuperFocus.ts similarity index 86% rename from packages/@uppy/dashboard/src/utils/createSuperFocus.js rename to packages/@uppy/dashboard/src/utils/createSuperFocus.ts index 6c7d178134..9ea865748c 100644 --- a/packages/@uppy/dashboard/src/utils/createSuperFocus.js +++ b/packages/@uppy/dashboard/src/utils/createSuperFocus.ts @@ -1,6 +1,10 @@ import debounce from 'lodash/debounce.js' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore untyped import FOCUSABLE_ELEMENTS from '@uppy/utils/lib/FOCUSABLE_ELEMENTS' -import getActiveOverlayEl from './getActiveOverlayEl.js' +import getActiveOverlayEl from './getActiveOverlayEl.ts' + +type $TSFixMe = any /* Focuses on some element in the currently topmost overlay. @@ -12,10 +16,10 @@ import getActiveOverlayEl from './getActiveOverlayEl.js' 2. If there are no [data-uppy-super-focusable] elements yet (or ever) - focuses on the first focusable element, but switches focus if superfocusable elements appear on next render. */ -export default function createSuperFocus () { +export default function createSuperFocus(): $TSFixMe { let lastFocusWasOnSuperFocusableEl = false - const superFocus = (dashboardEl, activeOverlayType) => { + const superFocus = (dashboardEl: $TSFixMe, activeOverlayType: $TSFixMe) => { const overlayEl = getActiveOverlayEl(dashboardEl, activeOverlayType) const isFocusInOverlay = overlayEl.contains(document.activeElement) @@ -24,7 +28,9 @@ export default function createSuperFocus () { // [Practical check] without this line, typing in the search input in googledrive overlay won't work. if (isFocusInOverlay && lastFocusWasOnSuperFocusableEl) return - const superFocusableEl = overlayEl.querySelector('[data-uppy-super-focusable]') + const superFocusableEl = overlayEl.querySelector( + '[data-uppy-super-focusable]', + ) // If we are already in the topmost overlay, AND there are no super focusable elements yet, - leave focus up to the user. // [Practical check] without this line, if you are in an empty folder in google drive, and something's uploading in the // bg, - focus will be jumping to Done all the time. diff --git a/packages/@uppy/dashboard/src/utils/getActiveOverlayEl.js b/packages/@uppy/dashboard/src/utils/getActiveOverlayEl.js deleted file mode 100644 index 7a73ee5b25..0000000000 --- a/packages/@uppy/dashboard/src/utils/getActiveOverlayEl.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @returns {HTMLElement} - either dashboard element, or the overlay that's most on top - */ -export default function getActiveOverlayEl (dashboardEl, activeOverlayType) { - if (activeOverlayType) { - const overlayEl = dashboardEl.querySelector(`[data-uppy-paneltype="${activeOverlayType}"]`) - // if an overlay is already mounted - if (overlayEl) return overlayEl - } - return dashboardEl -} diff --git a/packages/@uppy/dashboard/src/utils/getActiveOverlayEl.ts b/packages/@uppy/dashboard/src/utils/getActiveOverlayEl.ts new file mode 100644 index 0000000000..2adc29b95d --- /dev/null +++ b/packages/@uppy/dashboard/src/utils/getActiveOverlayEl.ts @@ -0,0 +1,18 @@ +type $TSFixMe = any + +/** + * @returns {HTMLElement} - either dashboard element, or the overlay that's most on top + */ +export default function getActiveOverlayEl( + dashboardEl: $TSFixMe, + activeOverlayType: $TSFixMe, +): $TSFixMe { + if (activeOverlayType) { + const overlayEl = dashboardEl.querySelector( + `[data-uppy-paneltype="${activeOverlayType}"]`, + ) + // if an overlay is already mounted + if (overlayEl) return overlayEl + } + return dashboardEl +} diff --git a/packages/@uppy/dashboard/src/utils/getFileTypeIcon.jsx b/packages/@uppy/dashboard/src/utils/getFileTypeIcon.jsx deleted file mode 100644 index ad5a9445c1..0000000000 --- a/packages/@uppy/dashboard/src/utils/getFileTypeIcon.jsx +++ /dev/null @@ -1,127 +0,0 @@ -import { h } from 'preact' - -function iconImage () { - return ( - - ) -} - -function iconAudio () { - return ( - - ) -} - -function iconVideo () { - return ( - - ) -} - -function iconPDF () { - return ( - - ) -} - -function iconArchive () { - return ( - - ) -} - -function iconFile () { - return ( - - ) -} - -function iconText () { - return ( - - ) -} - -export default function getIconByMime (fileType) { - const defaultChoice = { - color: '#838999', - icon: iconFile(), - } - - if (!fileType) return defaultChoice - - const fileTypeGeneral = fileType.split('/')[0] - const fileTypeSpecific = fileType.split('/')[1] - - // Text - if (fileTypeGeneral === 'text') { - return { - color: '#5a5e69', - icon: iconText(), - } - } - - // Image - if (fileTypeGeneral === 'image') { - return { - color: '#686de0', - icon: iconImage(), - } - } - - // Audio - if (fileTypeGeneral === 'audio') { - return { - color: '#068dbb', - icon: iconAudio(), - } - } - - // Video - if (fileTypeGeneral === 'video') { - return { - color: '#19af67', - icon: iconVideo(), - } - } - - // PDF - if (fileTypeGeneral === 'application' && fileTypeSpecific === 'pdf') { - return { - color: '#e25149', - icon: iconPDF(), - } - } - - // Archive - const archiveTypes = ['zip', 'x-7z-compressed', 'x-rar-compressed', 'x-tar', 'x-gzip', 'x-apple-diskimage'] - if (fileTypeGeneral === 'application' && archiveTypes.indexOf(fileTypeSpecific) !== -1) { - return { - color: '#00C469', - icon: iconArchive(), - } - } - - return defaultChoice -} diff --git a/packages/@uppy/dashboard/src/utils/getFileTypeIcon.tsx b/packages/@uppy/dashboard/src/utils/getFileTypeIcon.tsx new file mode 100644 index 0000000000..51ddd39d4c --- /dev/null +++ b/packages/@uppy/dashboard/src/utils/getFileTypeIcon.tsx @@ -0,0 +1,212 @@ +import { h } from 'preact' + +type $TSFixMe = any + +function iconImage() { + return ( + + ) +} + +function iconAudio() { + return ( + + ) +} + +function iconVideo() { + return ( + + ) +} + +function iconPDF() { + return ( + + ) +} + +function iconArchive() { + return ( + + ) +} + +function iconFile() { + return ( + + ) +} + +function iconText() { + return ( + + ) +} + +export default function getIconByMime(fileType: $TSFixMe): $TSFixMe { + const defaultChoice = { + color: '#838999', + icon: iconFile(), + } + + if (!fileType) return defaultChoice + + const fileTypeGeneral = fileType.split('/')[0] + const fileTypeSpecific = fileType.split('/')[1] + + // Text + if (fileTypeGeneral === 'text') { + return { + color: '#5a5e69', + icon: iconText(), + } + } + + // Image + if (fileTypeGeneral === 'image') { + return { + color: '#686de0', + icon: iconImage(), + } + } + + // Audio + if (fileTypeGeneral === 'audio') { + return { + color: '#068dbb', + icon: iconAudio(), + } + } + + // Video + if (fileTypeGeneral === 'video') { + return { + color: '#19af67', + icon: iconVideo(), + } + } + + // PDF + if (fileTypeGeneral === 'application' && fileTypeSpecific === 'pdf') { + return { + color: '#e25149', + icon: iconPDF(), + } + } + + // Archive + const archiveTypes = [ + 'zip', + 'x-7z-compressed', + 'x-rar-compressed', + 'x-tar', + 'x-gzip', + 'x-apple-diskimage', + ] + if ( + fileTypeGeneral === 'application' && + archiveTypes.indexOf(fileTypeSpecific) !== -1 + ) { + return { + color: '#00C469', + icon: iconArchive(), + } + } + + return defaultChoice +} diff --git a/packages/@uppy/dashboard/src/utils/ignoreEvent.js b/packages/@uppy/dashboard/src/utils/ignoreEvent.ts similarity index 77% rename from packages/@uppy/dashboard/src/utils/ignoreEvent.js rename to packages/@uppy/dashboard/src/utils/ignoreEvent.ts index 85d7bd8456..9a4973bb64 100644 --- a/packages/@uppy/dashboard/src/utils/ignoreEvent.js +++ b/packages/@uppy/dashboard/src/utils/ignoreEvent.ts @@ -3,10 +3,11 @@ // draging UI elements or pasting anything into any field triggers those events — // Url treats them as URLs that need to be imported -function ignoreEvent (ev) { +type $TSFixMe = any + +function ignoreEvent(ev: $TSFixMe): void { const { tagName } = ev.target - if (tagName === 'INPUT' - || tagName === 'TEXTAREA') { + if (tagName === 'INPUT' || tagName === 'TEXTAREA') { ev.stopPropagation() return } diff --git a/packages/@uppy/dashboard/src/utils/trapFocus.js b/packages/@uppy/dashboard/src/utils/trapFocus.ts similarity index 70% rename from packages/@uppy/dashboard/src/utils/trapFocus.js rename to packages/@uppy/dashboard/src/utils/trapFocus.ts index 66353c22cd..84248498c0 100644 --- a/packages/@uppy/dashboard/src/utils/trapFocus.js +++ b/packages/@uppy/dashboard/src/utils/trapFocus.ts @@ -1,8 +1,12 @@ import toArray from '@uppy/utils/lib/toArray' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore untyped import FOCUSABLE_ELEMENTS from '@uppy/utils/lib/FOCUSABLE_ELEMENTS' -import getActiveOverlayEl from './getActiveOverlayEl.js' +import getActiveOverlayEl from './getActiveOverlayEl.ts' -function focusOnFirstNode (event, nodes) { +type $TSFixMe = any + +function focusOnFirstNode(event: $TSFixMe, nodes: $TSFixMe) { const node = nodes[0] if (node) { node.focus() @@ -10,7 +14,7 @@ function focusOnFirstNode (event, nodes) { } } -function focusOnLastNode (event, nodes) { +function focusOnLastNode(event: $TSFixMe, nodes: $TSFixMe) { const node = nodes[nodes.length - 1] if (node) { node.focus() @@ -24,13 +28,19 @@ function focusOnLastNode (event, nodes) { // active overlay! // [Practical check] if we use (focusedItemIndex === -1), instagram provider in firefox will never get focus on its pics // in the
    . -function isFocusInOverlay (activeOverlayEl) { +function isFocusInOverlay(activeOverlayEl: $TSFixMe) { return activeOverlayEl.contains(document.activeElement) } -function trapFocus (event, activeOverlayType, dashboardEl) { +function trapFocus( + event: $TSFixMe, + activeOverlayType: $TSFixMe, + dashboardEl: $TSFixMe, +): void { const activeOverlayEl = getActiveOverlayEl(dashboardEl, activeOverlayType) - const focusableNodes = toArray(activeOverlayEl.querySelectorAll(FOCUSABLE_ELEMENTS)) + const focusableNodes = toArray( + activeOverlayEl.querySelectorAll(FOCUSABLE_ELEMENTS), + ) const focusedItemIndex = focusableNodes.indexOf(document.activeElement) @@ -40,21 +50,28 @@ function trapFocus (event, activeOverlayType, dashboardEl) { // plugins will try to focus on some important element as it loads. if (!isFocusInOverlay(activeOverlayEl)) { focusOnFirstNode(event, focusableNodes) - // If we pressed shift + tab, and we're on the first element of a modal + // If we pressed shift + tab, and we're on the first element of a modal } else if (event.shiftKey && focusedItemIndex === 0) { focusOnLastNode(event, focusableNodes) - // If we pressed tab, and we're on the last element of the modal - } else if (!event.shiftKey && focusedItemIndex === focusableNodes.length - 1) { + // If we pressed tab, and we're on the last element of the modal + } else if ( + !event.shiftKey && + focusedItemIndex === focusableNodes.length - 1 + ) { focusOnFirstNode(event, focusableNodes) } } // Traps focus inside of the currently open overlay (e.g. Dashboard, or e.g. Instagram), // never lets focus disappear from the modal. -export { trapFocus as forModal } +export { trapFocus as forModal } // Traps focus inside of the currently open overlay, unless overlay is null - then let the user tab away. -export function forInline (event, activeOverlayType, dashboardEl) { +export function forInline( + event: $TSFixMe, + activeOverlayType: $TSFixMe, + dashboardEl: $TSFixMe, +): void { // ___When we're in the bare 'Drop files here, paste, browse or import from' screen if (activeOverlayType === null) { // Do nothing and let the browser handle it, user can tab away from Uppy to other elements on the page diff --git a/packages/@uppy/dashboard/tsconfig.build.json b/packages/@uppy/dashboard/tsconfig.build.json new file mode 100644 index 0000000000..e1c431130b --- /dev/null +++ b/packages/@uppy/dashboard/tsconfig.build.json @@ -0,0 +1,60 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "noImplicitAny": false, + "outDir": "./lib", + "paths": { + "@uppy/informer": ["../informer/src/index.js"], + "@uppy/informer/lib/*": ["../informer/src/*"], + "@uppy/provider-views": ["../provider-views/src/index.js"], + "@uppy/provider-views/lib/*": ["../provider-views/src/*"], + "@uppy/status-bar": ["../status-bar/src/index.js"], + "@uppy/status-bar/lib/*": ["../status-bar/src/*"], + "@uppy/thumbnail-generator": ["../thumbnail-generator/src/index.js"], + "@uppy/thumbnail-generator/lib/*": ["../thumbnail-generator/src/*"], + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"], + "@uppy/google-drive": ["../google-drive/src/index.js"], + "@uppy/google-drive/lib/*": ["../google-drive/src/*"], + "@uppy/url": ["../url/src/index.js"], + "@uppy/url/lib/*": ["../url/src/*"], + "@uppy/webcam": ["../webcam/src/index.js"], + "@uppy/webcam/lib/*": ["../webcam/src/*"] + }, + "resolveJsonModule": false, + "rootDir": "./src", + "skipLibCheck": true + }, + "include": ["./src/**/*.*"], + "exclude": ["./src/**/*.test.ts"], + "references": [ + { + "path": "../informer/tsconfig.build.json" + }, + { + "path": "../provider-views/tsconfig.build.json" + }, + { + "path": "../status-bar/tsconfig.build.json" + }, + { + "path": "../thumbnail-generator/tsconfig.build.json" + }, + { + "path": "../utils/tsconfig.build.json" + }, + { + "path": "../core/tsconfig.build.json" + }, + { + "path": "../google-drive/tsconfig.build.json" + }, + { + "path": "../url/tsconfig.build.json" + }, + { + "path": "../webcam/tsconfig.build.json" + } + ] +} diff --git a/packages/@uppy/dashboard/tsconfig.json b/packages/@uppy/dashboard/tsconfig.json new file mode 100644 index 0000000000..46cc2296b0 --- /dev/null +++ b/packages/@uppy/dashboard/tsconfig.json @@ -0,0 +1,56 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "emitDeclarationOnly": false, + "noEmit": true, + "paths": { + "@uppy/informer": ["../informer/src/index.js"], + "@uppy/informer/lib/*": ["../informer/src/*"], + "@uppy/provider-views": ["../provider-views/src/index.js"], + "@uppy/provider-views/lib/*": ["../provider-views/src/*"], + "@uppy/status-bar": ["../status-bar/src/index.js"], + "@uppy/status-bar/lib/*": ["../status-bar/src/*"], + "@uppy/thumbnail-generator": ["../thumbnail-generator/src/index.js"], + "@uppy/thumbnail-generator/lib/*": ["../thumbnail-generator/src/*"], + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"], + "@uppy/google-drive": ["../google-drive/src/index.js"], + "@uppy/google-drive/lib/*": ["../google-drive/src/*"], + "@uppy/url": ["../url/src/index.js"], + "@uppy/url/lib/*": ["../url/src/*"], + "@uppy/webcam": ["../webcam/src/index.js"], + "@uppy/webcam/lib/*": ["../webcam/src/*"], + }, + }, + "include": ["./package.json", "./src/**/*.*"], + "references": [ + { + "path": "../informer/tsconfig.build.json", + }, + { + "path": "../provider-views/tsconfig.build.json", + }, + { + "path": "../status-bar/tsconfig.build.json", + }, + { + "path": "../thumbnail-generator/tsconfig.build.json", + }, + { + "path": "../utils/tsconfig.build.json", + }, + { + "path": "../core/tsconfig.build.json", + }, + { + "path": "../google-drive/tsconfig.build.json", + }, + { + "path": "../url/tsconfig.build.json", + }, + { + "path": "../webcam/tsconfig.build.json", + }, + ], +}