1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483 |
- 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.js'
- 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'
- import findAllDOMElements from '@uppy/utils/lib/findAllDOMElements'
- import toArray from '@uppy/utils/lib/toArray'
- import getDroppedFiles from '@uppy/utils/lib/getDroppedFiles'
- import { defaultPickerIcon } from '@uppy/provider-views'
- import { nanoid } from 'nanoid/non-secure'
- import memoizeOne from 'memoize-one'
- 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.ts'
- type GenericEventCallback = () => void
- export type DashboardFileEditStartCallback<M extends Meta, B extends Body> = (
- file?: UppyFile<M, B>,
- ) => void
- export type DashboardFileEditCompleteCallback<
- M extends Meta,
- B extends Body,
- > = (file?: UppyFile<M, B>) => void
- export type DashboardShowPlanelCallback = (id: string) => void
- declare module '@uppy/core' {
- export interface UppyEventMap<M extends Meta, B extends Body> {
- 'dashboard:modal-open': GenericEventCallback
- 'dashboard:modal-closed': GenericEventCallback
- 'dashboard:show-panel': DashboardShowPlanelCallback
- 'dashboard:file-edit-start': DashboardFileEditStartCallback<M, B>
- 'dashboard:file-edit-complete': DashboardFileEditCompleteCallback<M, B>
- 'dashboard:close-panel': (id: string | undefined) => void
- 'restore-canceled': GenericEventCallback
- }
- }
- interface PromiseWithResolvers<T> {
- promise: Promise<T>
- resolve: (value: T | PromiseLike<T>) => void
- reject: (reason?: any) => void
- }
- const memoize = ((memoizeOne as any).default as false) || memoizeOne
- const TAB_KEY = 9
- const ESC_KEY = 27
- function createPromise<T>(): PromiseWithResolvers<T> {
- const o = {} as PromiseWithResolvers<T>
- o.promise = new Promise<T>((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<string, unknown> | null,
- ...children: any[]
- ) => VNode<any>
- interface MetaField {
- id: string
- name: string
- placeholder?: string
- render?: (field: FieldRenderOptions, h: PreactRender) => VNode<any>
- }
- interface Target {
- id: string
- name: string
- type: string
- }
- interface TargetWithRender extends Target {
- icon: ComponentChild
- render: () => ComponentChild
- }
- export interface DashboardState<M extends Meta, B extends Body> {
- targets: Target[]
- activePickerPanel: Target | undefined
- showAddFilesPanel: boolean
- activeOverlayType: string | null
- fileCardFor: string | null
- showFileEditor: boolean
- metaFields?: MetaField[] | ((file: UppyFile<M, B>) => MetaField[])
- [key: string]: unknown
- }
- export interface DashboardModalOptions {
- inline?: false
- animateOpenClose?: boolean
- browserBackButtonClose?: boolean
- closeAfterFinish?: boolean
- closeModalOnClickOutside?: boolean
- disablePageScrollWhenModalOpen?: boolean
- }
- export interface DashboardInlineOptions {
- inline: true
- height?: string | number
- width?: string | number
- }
- interface DashboardMiscOptions<M extends Meta, B extends Body>
- extends UIPluginOptions {
- autoOpen?: 'metaEditor' | 'imageEditor' | null
- defaultPickerIcon?: typeof defaultPickerIcon
- disabled?: boolean
- disableInformer?: boolean
- disableLocalFiles?: boolean
- disableStatusBar?: boolean
- disableThumbnailGenerator?: boolean
- doneButtonHandler?: null | (() => void)
- fileManagerSelectionType?: 'files' | 'folders' | 'both'
- hideCancelButton?: boolean
- hidePauseResumeButton?: boolean
- hideProgressAfterFinish?: boolean
- hideRetryButton?: boolean
- hideUploadButton?: boolean
- metaFields?: MetaField[] | ((file: UppyFile<M, B>) => MetaField[])
- nativeCameraFacingMode?: ConstrainDOMString
- note?: string | null
- onDragLeave?: (event: DragEvent) => void
- onDragOver?: (event: DragEvent) => void
- onDrop?: (event: DragEvent) => void
- onRequestCloseModal?: () => void
- plugins?: string[]
- proudlyDisplayPoweredByUppy?: boolean
- showLinkToFileUploadResult?: boolean
- showNativePhotoCameraButton?: boolean
- showNativeVideoCameraButton?: boolean
- showProgressDetails?: boolean
- showRemoveButtonAfterComplete?: boolean
- showSelectedFiles?: boolean
- singleFileFullScreen?: boolean
- theme?: 'auto' | 'dark' | 'light'
- thumbnailHeight?: number
- thumbnailType?: string
- thumbnailWidth?: number
- trigger?: string | Element
- waitForThumbnailsBeforeUpload?: boolean
- }
- export type DashboardOptions<
- M extends Meta,
- B extends Body,
- > = DashboardMiscOptions<M, B> &
- (DashboardModalOptions | DashboardInlineOptions)
- const defaultOptions = {
- target: 'body',
- metaFields: [],
- inline: false as boolean,
- 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',
- autoOpen: null,
- 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: undefined as any,
- onRequestCloseModal: null as any,
- } satisfies Partial<DashboardOptions<any, any>>
- /**
- * Dashboard UI with previews, metadata editing, tabs for various services and more
- */
- export default class Dashboard<M extends Meta, B extends Body> extends UIPlugin<
- DefinePluginOpts<
- // The options object inside the class is not the discriminated union but and intersection of the different subtypes.
- DashboardMiscOptions<M, B> &
- Omit<DashboardInlineOptions, 'inline'> &
- Omit<DashboardModalOptions, 'inline'> & { inline?: boolean },
- keyof typeof defaultOptions
- >,
- M,
- B,
- DashboardState<M, B>
- > {
- static VERSION = packageJson.version
- #disabledNodes!: HTMLElement[] | null
- private modalName = `uppy-Dashboard-${nanoid()}`
- private superFocus = createSuperFocus()
- private ifFocusedOnUppyRecently = false
- private dashboardIsDisabled!: boolean
- private savedScrollPosition!: number
- private savedActiveElement!: HTMLElement
- private resizeObserver!: ResizeObserver
- private darkModeMediaQuery!: MediaQueryList | null
- // Timeouts
- private makeDashboardInsidesVisibleAnywayTimeout!: ReturnType<
- typeof setTimeout
- >
- constructor(uppy: Uppy<M, B>, opts?: DashboardOptions<M, B>) {
- const autoOpen = opts?.autoOpen ?? null
- super(uppy, { ...defaultOptions, ...opts, autoOpen })
- this.id = this.opts.id || 'Dashboard'
- this.title = 'Dashboard'
- this.type = 'orchestrator'
- this.defaultLocale = locale
- // Dynamic default options:
- if (this.opts.doneButtonHandler === undefined) {
- // `null` means "do not display a Done button", while `undefined` means
- // "I want the default behavior". For this reason, we need to differentiate `null` and `undefined`.
- this.opts.doneButtonHandler = () => {
- this.uppy.clear()
- this.requestCloseModal()
- }
- }
- this.opts.onRequestCloseModal ??= () => this.closeModal()
- this.i18nInit()
- }
- removeTarget = (plugin: UnknownPlugin<M, B>): void => {
- const pluginState = this.getPluginState()
- // filter out the one we want to remove
- const newTargets = pluginState.targets.filter(
- (target) => target.id !== plugin.id,
- )
- this.setPluginState({
- targets: newTargets,
- })
- }
- addTarget = (plugin: UnknownPlugin<M, B>): HTMLElement | null => {
- const callerPluginId = plugin.id || plugin.constructor.name
- 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'
- this.uppy.log(msg, 'error')
- return null
- }
- const target = {
- id: callerPluginId,
- name: callerPluginName,
- type: callerPluginType,
- }
- const state = this.getPluginState()
- const newTargets = state.targets.slice()
- newTargets.push(target)
- this.setPluginState({
- targets: newTargets,
- })
- return this.el
- }
- hideAllPanels = (): void => {
- const state = this.getPluginState()
- const update = {
- 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
- ) {
- // avoid doing a state update if nothing changed
- return
- }
- this.setPluginState(update)
- this.uppy.emit('dashboard:close-panel', state.activePickerPanel?.id)
- }
- showPanel = (id: string): void => {
- const { targets } = this.getPluginState()
- const activePickerPanel = targets.find((target) => {
- return target.type === 'acquirer' && target.id === id
- })
- this.setPluginState({
- activePickerPanel,
- activeOverlayType: 'PickerPanel',
- })
- this.uppy.emit('dashboard:show-panel', id)
- }
- private canEditFile = (file: UppyFile<M, B>): boolean => {
- const { targets } = this.getPluginState()
- const editors = this.#getEditors(targets)
- return editors.some((target) =>
- (this.uppy.getPlugin(target.id) as any).canEditFile(file),
- )
- }
- openFileEditor = (file: UppyFile<M, B>): void => {
- const { targets } = this.getPluginState()
- const editors = this.#getEditors(targets)
- this.setPluginState({
- showFileEditor: true,
- fileCardFor: file.id || null,
- activeOverlayType: 'FileEditor',
- })
- editors.forEach((editor) => {
- ;(this.uppy.getPlugin(editor.id) as any).selectFile(file)
- })
- }
- closeFileEditor = (): void => {
- const { metaFields } = this.getPluginState()
- const isMetaEditorEnabled = metaFields && metaFields.length > 0
- if (isMetaEditorEnabled) {
- this.setPluginState({
- showFileEditor: false,
- activeOverlayType: 'FileCard',
- })
- } else {
- this.setPluginState({
- showFileEditor: false,
- fileCardFor: null,
- activeOverlayType: 'AddFiles',
- })
- }
- }
- saveFileEditor = (): void => {
- const { targets } = this.getPluginState()
- const editors = this.#getEditors(targets)
- editors.forEach((editor) => {
- ;(this.uppy.getPlugin(editor.id) as any).save()
- })
- this.closeFileEditor()
- }
- openModal = (): Promise<void> => {
- const { promise, resolve } = createPromise<void>()
- // save scroll position
- this.savedScrollPosition = window.pageYOffset
- // save active element, so we can restore focus when modal is closed
- this.savedActiveElement = document.activeElement as HTMLElement
- if (this.opts.disablePageScrollWhenModalOpen) {
- document.body.classList.add('uppy-Dashboard-isFixed')
- }
- if (this.opts.animateOpenClose && this.getPluginState().isClosing) {
- const handler = () => {
- this.setPluginState({
- isHidden: false,
- })
- this.el!.removeEventListener('animationend', handler, false)
- resolve()
- }
- this.el!.addEventListener('animationend', handler, false)
- } else {
- this.setPluginState({
- isHidden: false,
- })
- resolve()
- }
- if (this.opts.browserBackButtonClose) {
- this.updateBrowserHistory()
- }
- // handle ESC and TAB keys in modal dialog
- document.addEventListener('keydown', this.handleKeyDownInModal)
- this.uppy.emit('dashboard:modal-open')
- return promise
- }
- closeModal = (opts?: { manualClose: boolean }): void | Promise<void> => {
- // 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) {
- // short-circuit if animation is ongoing
- return undefined
- }
- const { promise, resolve } = createPromise<void>()
- if (this.opts.disablePageScrollWhenModalOpen) {
- document.body.classList.remove('uppy-Dashboard-isFixed')
- }
- if (this.opts.animateOpenClose) {
- this.setPluginState({
- isClosing: true,
- })
- const handler = () => {
- this.setPluginState({
- isHidden: true,
- isClosing: false,
- })
- this.superFocus.cancel()
- this.savedActiveElement.focus()
- this.el!.removeEventListener('animationend', handler, false)
- resolve()
- }
- this.el!.addEventListener('animationend', handler, false)
- } else {
- this.setPluginState({
- isHidden: true,
- })
- this.superFocus.cancel()
- this.savedActiveElement.focus()
- resolve()
- }
- // handle ESC and TAB keys in modal dialog
- document.removeEventListener('keydown', this.handleKeyDownInModal)
- if (manualClose) {
- if (this.opts.browserBackButtonClose) {
- // Make sure that the latest entry in the history state is our modal name
- // eslint-disable-next-line no-restricted-globals
- if (history.state?.[this.modalName]) {
- // Go back in history to clear out the entry we created (ultimately closing the modal)
- // eslint-disable-next-line no-restricted-globals
- history.back()
- }
- }
- }
- this.uppy.emit('dashboard:modal-closed')
- return promise
- }
- isModalOpen = (): boolean => {
- return !this.getPluginState().isHidden || false
- }
- private requestCloseModal = (): void | Promise<void> => {
- if (this.opts.onRequestCloseModal) {
- return this.opts.onRequestCloseModal()
- }
- return this.closeModal()
- }
- setDarkModeCapability = (isDarkModeOn: boolean): void => {
- const { capabilities } = this.uppy.getState()
- this.uppy.setState({
- capabilities: {
- ...capabilities,
- darkMode: isDarkModeOn,
- },
- })
- }
- private handleSystemDarkModeChange = (event: MediaQueryListEvent) => {
- const isDarkModeOnNow = event.matches
- this.uppy.log(`[Dashboard] Dark mode is ${isDarkModeOnNow ? 'on' : 'off'}`)
- this.setDarkModeCapability(isDarkModeOnNow)
- }
- private toggleFileCard = (show: boolean, fileID: string) => {
- const file = this.uppy.getFile(fileID)
- if (show) {
- this.uppy.emit('dashboard:file-edit-start', file)
- } else {
- this.uppy.emit('dashboard:file-edit-complete', file)
- }
- this.setPluginState({
- fileCardFor: show ? fileID : null,
- activeOverlayType: show ? 'FileCard' : null,
- })
- }
- private toggleAddFilesPanel = (show: boolean) => {
- this.setPluginState({
- showAddFilesPanel: show,
- activeOverlayType: show ? 'AddFiles' : null,
- })
- }
- addFiles = (files: File[]): void => {
- const descriptors = files.map((file) => ({
- source: this.id,
- name: file.name,
- type: file.type,
- data: file,
- meta: {
- // path of the file relative to the ancestor directory the user selected.
- // e.g. 'docs/Old Prague/airbnb.pdf'
- relativePath:
- (file as any).relativePath || file.webkitRelativePath || null,
- } as any as M,
- }))
- try {
- this.uppy.addFiles(descriptors)
- } catch (err) {
- this.uppy.log(err as any)
- }
- }
- // ___Why make insides of Dashboard invisible until first ResizeObserver event is emitted?
- // ResizeOberserver doesn't emit the first resize event fast enough, users can see the jump from one .uppy-size-- to
- // another (e.g. in Safari)
- // ___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.
- private startListeningToResize = () => {
- // Watch for Dashboard container (`.uppy-Dashboard-inner`) resize
- // and update containerWidth/containerHeight in plugin state accordingly.
- // Emits first event on initialization.
- this.resizeObserver = new ResizeObserver((entries) => {
- const uppyDashboardInnerEl = entries[0]
- const { width, height } = uppyDashboardInnerEl.contentRect
- this.setPluginState({
- containerWidth: width,
- containerHeight: height,
- areInsidesReadyToBeVisible: true,
- })
- })
- 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 ResizeObserver hasn't yet fired,
- !pluginState.areInsidesReadyToBeVisible &&
- // and it's not due to the modal being closed
- !isModalAndClosed
- ) {
- this.uppy.log(
- '[Dashboard] resize event didn’t fire on time: defaulted to mobile layout',
- 'warning',
- )
- this.setPluginState({
- areInsidesReadyToBeVisible: true,
- })
- }
- }, 1000)
- }
- private stopListeningToResize = () => {
- this.resizeObserver.disconnect()
- clearTimeout(this.makeDashboardInsidesVisibleAnywayTimeout)
- }
- // Records whether we have been interacting with uppy right now,
- // which is then used to determine whether state updates should trigger a refocusing.
- private recordIfFocusedOnUppyRecently = (event: Event) => {
- if (this.el!.contains(event.target as HTMLElement)) {
- this.ifFocusedOnUppyRecently = true
- } else {
- this.ifFocusedOnUppyRecently = false
- // ___Why run this.superFocus.cancel here when it already runs in superFocusOnEachUpdate?
- // Because superFocus is debounced, when we move from Uppy to some other element on the page,
- // previously run superFocus sometimes hits and moves focus back to Uppy.
- this.superFocus.cancel()
- }
- }
- private disableInteractiveElements = (disable: boolean) => {
- const NODES_TO_DISABLE = [
- 'a[href]',
- 'input:not([disabled])',
- 'select:not([disabled])',
- 'textarea:not([disabled])',
- 'button:not([disabled])',
- '[role="button"]:not([disabled])',
- ]
- 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
- if (node.tagName === 'A') {
- node.setAttribute('aria-disabled', disable)
- } else {
- node.disabled = disable
- }
- }
- if (disable) {
- this.#disabledNodes = nodesToDisable
- } else {
- this.#disabledNodes = null
- }
- this.dashboardIsDisabled = disable
- }
- 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,
- },
- '',
- )
- }
- // Listen for back button presses
- window.addEventListener('popstate', this.handlePopState, false)
- }
- 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 })
- }
- // When the browser back button is pressed and uppy is now the latest entry
- // in the history but the modal is closed, fix the history by removing the
- // uppy history entry.
- // This occurs when another entry is added into the history state while the
- // modal is open, and then the modal gets manually closed.
- // Solves PR #575 (https://github.com/transloadit/uppy/pull/575)
- if (!this.isModalOpen() && event.state?.[this.modalName]) {
- // eslint-disable-next-line no-restricted-globals
- history.back()
- }
- }
- private handleKeyDownInModal = (event: KeyboardEvent) => {
- // close modal on esc key press
- 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,
- )
- }
- private handleClickOutside = () => {
- if (this.opts.closeModalOnClickOutside) this.requestCloseModal()
- }
- 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 as any).handleRootPaste?.(event)
- }
- })
- // Add all dropped files
- const files = toArray(event.clipboardData!.files)
- if (files.length > 0) {
- this.uppy.log('[Dashboard] Files pasted')
- this.addFiles(files)
- }
- }
- private handleInputChange = (event: InputEvent) => {
- event.preventDefault()
- const files = toArray((event.target as HTMLInputElement).files!)
- if (files.length > 0) {
- this.uppy.log('[Dashboard] Files selected through input')
- this.addFiles(files)
- }
- }
- private handleDragOver = (event: DragEvent) => {
- event.preventDefault()
- event.stopPropagation()
- // Check if some plugin can handle the datatransfer without files —
- // for instance, the Url plugin can import a url
- const canSomePluginHandleRootDrop = () => {
- let somePluginCanHandleRootDrop = true
- this.uppy.iteratePlugins((plugin) => {
- if ((plugin as any).canHandleRootDrop?.(event)) {
- somePluginCanHandleRootDrop = true
- }
- })
- return somePluginCanHandleRootDrop
- }
- // Check if the "type" of the datatransfer object includes files
- const doesEventHaveFiles = () => {
- 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()
- const hasFiles = doesEventHaveFiles()
- if (
- (!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
- ) {
- event.dataTransfer!.dropEffect = 'none' // eslint-disable-line no-param-reassign
- return
- }
- // 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
- this.setPluginState({ isDraggingOver: true })
- this.opts.onDragOver?.(event)
- }
- private handleDragLeave = (event: DragEvent) => {
- event.preventDefault()
- event.stopPropagation()
- this.setPluginState({ isDraggingOver: false })
- this.opts.onDragLeave?.(event)
- }
- private handleDrop = async (event: DragEvent) => {
- event.preventDefault()
- event.stopPropagation()
- this.setPluginState({ isDraggingOver: false })
- // Let any acquirer plugin (Url/Webcam/etc.) handle drops to the root
- this.uppy.iteratePlugins((plugin) => {
- if (plugin.type === 'acquirer') {
- // Every Plugin with .type acquirer can define handleRootDrop(event)
- ;(plugin as any).handleRootDrop?.(event)
- }
- })
- // Add all dropped files
- let executedDropErrorOnce = false
- const logDropError = (error: any) => {
- this.uppy.log(error, 'error')
- // In practice all drop errors are most likely the same,
- // so let's just show one to avoid overwhelming the user
- if (!executedDropErrorOnce) {
- this.uppy.info(error.message, 'error')
- executedDropErrorOnce = true
- }
- }
- this.uppy.log('[Dashboard] Processing dropped files')
- // Add all dropped files
- const files = await getDroppedFiles(event.dataTransfer!, { logDropError })
- if (files.length > 0) {
- this.uppy.log('[Dashboard] Files dropped')
- this.addFiles(files)
- }
- this.opts.onDrop?.(event)
- }
- private handleRequestThumbnail = (file: UppyFile<M, B>) => {
- if (!this.opts.waitForThumbnailsBeforeUpload) {
- this.uppy.emit('thumbnail:request', file)
- }
- }
- /**
- * We cancel thumbnail requests when a file item component unmounts to avoid
- * clogging up the queue when the user scrolls past many elements.
- */
- private handleCancelThumbnail = (file: UppyFile<M, B>) => {
- if (!this.opts.waitForThumbnailsBeforeUpload) {
- this.uppy.emit('thumbnail:cancel', file)
- }
- }
- private handleKeyDownInInline = (event: KeyboardEvent) => {
- // Trap focus on tab key press.
- 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,
- // or this.el.addEventListener('paste')?
- // Because (at least) Chrome doesn't handle paste if focus is on some button, e.g. 'My Device'.
- // => Therefore, the best option is to listen to all 'paste' events, and only react to them when we are focused on our
- // particular Uppy instance.
- // ___Why do we still need onPaste={props.handlePaste} for the DashboardUi?
- // 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.
- private handlePasteOnBody = (event: ClipboardEvent) => {
- const isFocusInOverlay = this.el!.contains(document.activeElement)
- if (isFocusInOverlay) {
- this.handlePaste(event)
- }
- }
- private handleComplete = ({ failed }: UploadResult<M, B>) => {
- if (this.opts.closeAfterFinish && !failed?.length) {
- // All uploads are done
- this.requestCloseModal()
- }
- }
- private handleCancelRestore = () => {
- this.uppy.emit('restore-canceled')
- }
- #generateLargeThumbnailIfSingleFile = () => {
- if (this.opts.disableThumbnailGenerator) {
- return
- }
- const LARGE_THUMBNAIL = 600
- const files = this.uppy.getFiles()
- if (files.length === 1) {
- const thumbnailGenerator = this.uppy.getPlugin(
- `${this.id}:ThumbnailGenerator`,
- ) as ThumbnailGenerator<M, B> | undefined
- thumbnailGenerator?.setOptions({ thumbnailWidth: LARGE_THUMBNAIL })
- const fileForThumbnail = { ...files[0], preview: undefined }
- thumbnailGenerator?.requestThumbnail(fileForThumbnail).then(() => {
- thumbnailGenerator?.setOptions({
- thumbnailWidth: this.opts.thumbnailWidth,
- })
- })
- }
- }
- #openFileEditorWhenFilesAdded = (files: UppyFile<M, B>[]) => {
- const firstFile = files[0]
- const { metaFields } = this.getPluginState()
- const isMetaEditorEnabled = metaFields && metaFields.length > 0
- const isImageEditorEnabled = this.canEditFile(firstFile)
- if (isMetaEditorEnabled && this.opts.autoOpen === 'metaEditor') {
- this.toggleFileCard(true, firstFile.id)
- } else if (isImageEditorEnabled && this.opts.autoOpen === 'imageEditor') {
- this.openFileEditor(firstFile)
- }
- }
- 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),
- )
- } 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.startListeningToResize()
- document.addEventListener('paste', this.handlePasteOnBody)
- this.uppy.on('plugin-added', this.#addSupportedPluginIfNoTarget)
- this.uppy.on('plugin-remove', this.removeTarget)
- this.uppy.on('file-added', this.hideAllPanels)
- this.uppy.on('dashboard:modal-closed', this.hideAllPanels)
- this.uppy.on('complete', this.handleComplete)
- this.uppy.on('files-added', this.#generateLargeThumbnailIfSingleFile)
- this.uppy.on('file-removed', this.#generateLargeThumbnailIfSingleFile)
- // ___Why fire on capture?
- // Because this.ifFocusedOnUppyRecently needs to change before onUpdate() fires.
- document.addEventListener('focus', this.recordIfFocusedOnUppyRecently, true)
- document.addEventListener('click', this.recordIfFocusedOnUppyRecently, true)
- if (this.opts.inline) {
- this.el!.addEventListener('keydown', this.handleKeyDownInInline)
- }
- if (this.opts.autoOpen) {
- this.uppy.on('files-added', this.#openFileEditorWhenFilesAdded)
- }
- }
- removeEvents = (): void => {
- const showModalTrigger = findAllDOMElements(this.opts.trigger)
- if (!this.opts.inline && showModalTrigger) {
- showModalTrigger.forEach((trigger) =>
- trigger.removeEventListener('click', this.openModal),
- )
- }
- this.stopListeningToResize()
- document.removeEventListener('paste', this.handlePasteOnBody)
- window.removeEventListener('popstate', this.handlePopState, false)
- this.uppy.off('plugin-added', this.#addSupportedPluginIfNoTarget)
- this.uppy.off('plugin-remove', this.removeTarget)
- this.uppy.off('file-added', this.hideAllPanels)
- this.uppy.off('dashboard:modal-closed', this.hideAllPanels)
- this.uppy.off('complete', this.handleComplete)
- this.uppy.off('files-added', this.#generateLargeThumbnailIfSingleFile)
- this.uppy.off('file-removed', this.#generateLargeThumbnailIfSingleFile)
- document.removeEventListener('focus', this.recordIfFocusedOnUppyRecently)
- document.removeEventListener('click', this.recordIfFocusedOnUppyRecently)
- if (this.opts.inline) {
- this.el!.removeEventListener('keydown', this.handleKeyDownInInline)
- }
- if (this.opts.autoOpen) {
- this.uppy.off('files-added', this.#openFileEditorWhenFilesAdded)
- }
- }
- 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 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 ||
- // If we are already inside of Uppy, or
- 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
- // something somewhere else on the page, - focus won't be 'nowhere'.
- // 2. We only focus when focus is nowhere AND this.ifFocusedOnUppyRecently,
- // to avoid focus jumps if we do something else on the page.
- // [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))
- ) {
- this.superFocus(this.el, this.getPluginState().activeOverlayType)
- } else {
- this.superFocus.cancel()
- }
- }
- readonly afterUpdate = (): void => {
- if (this.opts.disabled && !this.dashboardIsDisabled) {
- this.disableInteractiveElements(true)
- return
- }
- if (!this.opts.disabled && this.dashboardIsDisabled) {
- this.disableInteractiveElements(false)
- }
- this.superFocusOnEachUpdate()
- }
- saveFileCard = (meta: M, fileID: string): void => {
- this.uppy.setFileMeta(fileID, meta)
- this.toggleFileCard(false, fileID)
- }
- #attachRenderFunctionToTarget = (target: Target): TargetWithRender => {
- const plugin = this.uppy.getPlugin(target.id) as any
- return {
- ...target,
- icon: plugin.icon || this.opts.defaultPickerIcon,
- render: plugin.render,
- }
- }
- #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
- }
- return plugin.isSupported()
- }
- #getAcquirers = memoize((targets: Target[]) => {
- return targets
- .filter(
- (target) =>
- target.type === 'acquirer' && this.#isTargetSupported(target),
- )
- .map(this.#attachRenderFunctionToTarget)
- })
- #getProgressIndicators = memoize((targets: Target[]) => {
- return targets
- .filter((target) => target.type === 'progressindicator')
- .map(this.#attachRenderFunctionToTarget)
- })
- #getEditors = memoize((targets: Target[]) => {
- return targets
- .filter((target) => target.type === 'editor')
- .map(this.#attachRenderFunctionToTarget)
- })
- render = (state: State<M, B>) => {
- const pluginState = this.getPluginState()
- const { files, capabilities, allowNewUpload } = state
- const {
- newFiles,
- uploadStartedFiles,
- completeFiles,
- erroredFiles,
- inProgressFiles,
- inProgressNotPausedFiles,
- processingFiles,
- isUploadStarted,
- isAllComplete,
- isAllPaused,
- } = this.uppy.getObjectOfFilesPerState()
- const acquirers = this.#getAcquirers(pluginState.targets)
- const progressindicators = this.#getProgressIndicators(pluginState.targets)
- const editors = this.#getEditors(pluginState.targets)
- let theme
- if (this.opts.theme === 'auto') {
- theme = capabilities.darkMode ? 'dark' : 'light'
- } else {
- theme = this.opts.theme
- }
- 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}".`,
- )
- }
- return DashboardUI({
- state,
- isHidden: pluginState.isHidden,
- files,
- newFiles,
- uploadStartedFiles,
- completeFiles,
- erroredFiles,
- inProgressFiles,
- inProgressNotPausedFiles,
- processingFiles,
- isUploadStarted,
- isAllComplete,
- isAllPaused,
- totalFileCount: Object.keys(files).length,
- totalProgress: state.totalProgress,
- allowNewUpload,
- acquirers,
- theme,
- disabled: this.opts.disabled,
- disableLocalFiles: this.opts.disableLocalFiles,
- direction: this.opts.direction,
- activePickerPanel: pluginState.activePickerPanel,
- showFileEditor: pluginState.showFileEditor,
- saveFileEditor: this.saveFileEditor,
- closeFileEditor: this.closeFileEditor,
- disableInteractiveElements: this.disableInteractiveElements,
- animateOpenClose: this.opts.animateOpenClose,
- isClosing: pluginState.isClosing,
- progressindicators,
- editors,
- autoProceed: this.uppy.opts.autoProceed,
- id: this.id,
- closeModal: this.requestCloseModal,
- handleClickOutside: this.handleClickOutside,
- handleInputChange: this.handleInputChange,
- handlePaste: this.handlePaste,
- inline: this.opts.inline,
- showPanel: this.showPanel,
- hideAllPanels: this.hideAllPanels,
- i18n: this.i18n,
- i18nArray: this.i18nArray,
- uppy: this.uppy,
- note: this.opts.note,
- recoveredState: state.recoveredState,
- metaFields: pluginState.metaFields,
- resumableUploads: capabilities.resumableUploads || false,
- individualCancellation: capabilities.individualCancellation,
- isMobileDevice: capabilities.isMobileDevice,
- fileCardFor: pluginState.fileCardFor,
- toggleFileCard: this.toggleFileCard,
- toggleAddFilesPanel: this.toggleAddFilesPanel,
- showAddFilesPanel: pluginState.showAddFilesPanel,
- saveFileCard: this.saveFileCard,
- openFileEditor: this.openFileEditor,
- canEditFile: this.canEditFile,
- width: this.opts.width,
- height: this.opts.height,
- showLinkToFileUploadResult: this.opts.showLinkToFileUploadResult,
- fileManagerSelectionType: this.opts.fileManagerSelectionType,
- proudlyDisplayPoweredByUppy: this.opts.proudlyDisplayPoweredByUppy,
- hideCancelButton: this.opts.hideCancelButton,
- hideRetryButton: this.opts.hideRetryButton,
- hidePauseResumeButton: this.opts.hidePauseResumeButton,
- showRemoveButtonAfterComplete: this.opts.showRemoveButtonAfterComplete,
- containerWidth: pluginState.containerWidth,
- containerHeight: pluginState.containerHeight,
- areInsidesReadyToBeVisible: pluginState.areInsidesReadyToBeVisible,
- parentElement: this.el,
- allowedFileTypes: this.uppy.opts.restrictions.allowedFileTypes,
- maxNumberOfFiles: this.uppy.opts.restrictions.maxNumberOfFiles,
- requiredMetaFields: this.uppy.opts.restrictions.requiredMetaFields,
- showSelectedFiles: this.opts.showSelectedFiles,
- showNativePhotoCameraButton: this.opts.showNativePhotoCameraButton,
- showNativeVideoCameraButton: this.opts.showNativeVideoCameraButton,
- nativeCameraFacingMode: this.opts.nativeCameraFacingMode,
- singleFileFullScreen: this.opts.singleFileFullScreen,
- handleCancelRestore: this.handleCancelRestore,
- handleRequestThumbnail: this.handleRequestThumbnail,
- handleCancelThumbnail: this.handleCancelThumbnail,
- // drag props
- isDraggingOver: pluginState.isDraggingOver,
- handleDragOver: this.handleDragOver,
- handleDragLeave: this.handleDragLeave,
- handleDrop: this.handleDrop,
- })
- }
- #addSpecifiedPluginsFromOptions = () => {
- const plugins = this.opts.plugins || []
- plugins.forEach((pluginID) => {
- const plugin = this.uppy.getPlugin(pluginID)
- if (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',
- )
- }
- })
- }
- #autoDiscoverPlugins = () => {
- this.uppy.iteratePlugins(this.#addSupportedPluginIfNoTarget)
- }
- #addSupportedPluginIfNoTarget = (plugin?: UnknownPlugin<M, B>) => {
- // 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,
- )
- if (!pluginAlreadyAdded) {
- ;(plugin as any).mount(this, plugin)
- }
- }
- }
- #getStatusBarOpts() {
- const {
- hideUploadButton,
- hideRetryButton,
- hidePauseResumeButton,
- hideCancelButton,
- showProgressDetails,
- hideProgressAfterFinish,
- locale: l,
- doneButtonHandler,
- } = this.opts
- return {
- hideUploadButton,
- hideRetryButton,
- hidePauseResumeButton,
- hideCancelButton,
- showProgressDetails,
- hideAfterFinish: hideProgressAfterFinish,
- locale: l,
- doneButtonHandler,
- }
- }
- #getThumbnailGeneratorOpts() {
- const {
- thumbnailWidth,
- thumbnailHeight,
- thumbnailType,
- waitForThumbnailsBeforeUpload,
- } = this.opts
- return {
- thumbnailWidth,
- thumbnailHeight,
- thumbnailType,
- waitForThumbnailsBeforeUpload,
- // If we don't block on thumbnails, we can lazily generate them
- lazy: !waitForThumbnailsBeforeUpload,
- }
- }
- // eslint-disable-next-line class-methods-use-this
- #getInformerOpts() {
- return {
- // currently no options
- }
- }
- setOptions(opts: Partial<DashboardOptions<M, B>>) {
- super.setOptions(opts)
- this.uppy
- .getPlugin(this.#getStatusBarId())
- ?.setOptions(this.#getStatusBarOpts())
- this.uppy
- .getPlugin(this.#getThumbnailGeneratorId())
- ?.setOptions(this.#getThumbnailGeneratorOpts())
- }
- #getStatusBarId() {
- return `${this.id}:StatusBar`
- }
- #getThumbnailGeneratorId() {
- return `${this.id}:ThumbnailGenerator`
- }
- #getInformerId() {
- return `${this.id}:Informer`
- }
- install = (): void => {
- // Set default state for Dashboard
- this.setPluginState({
- isHidden: true,
- fileCardFor: null,
- activeOverlayType: null,
- showAddFilesPanel: false,
- activePickerPanel: undefined,
- showFileEditor: false,
- metaFields: this.opts.metaFields,
- targets: [],
- // We'll make them visible once .containerWidth is determined
- areInsidesReadyToBeVisible: false,
- isDraggingOver: false,
- })
- 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.',
- )
- }
- 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',
- )
- }
- const { target } = this.opts
- if (target) {
- this.mount(target, this)
- }
- if (!this.opts.disableStatusBar) {
- this.uppy.use(StatusBar, {
- id: this.#getStatusBarId(),
- target: this,
- ...this.#getStatusBarOpts(),
- })
- }
- if (!this.opts.disableInformer) {
- this.uppy.use(Informer, {
- id: this.#getInformerId(),
- target: this,
- ...this.#getInformerOpts(),
- })
- }
- if (!this.opts.disableThumbnailGenerator) {
- this.uppy.use(ThumbnailGenerator, {
- id: this.#getThumbnailGeneratorId(),
- ...this.#getThumbnailGeneratorOpts(),
- })
- }
- // Dark Mode / theme
- 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'}`,
- )
- this.setDarkModeCapability(isDarkModeOnFromTheStart)
- if (this.opts.theme === 'auto') {
- this.darkModeMediaQuery?.addListener(this.handleSystemDarkModeChange)
- }
- this.#addSpecifiedPluginsFromOptions()
- this.#autoDiscoverPlugins()
- this.initEvents()
- }
- 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
- // before the Dashboard was.
- if (informer) this.uppy.removePlugin(informer)
- }
- if (!this.opts.disableStatusBar) {
- const statusBar = this.uppy.getPlugin(`${this.id}:StatusBar`)
- if (statusBar) this.uppy.removePlugin(statusBar)
- }
- if (!this.opts.disableThumbnailGenerator) {
- const thumbnail = this.uppy.getPlugin(`${this.id}:ThumbnailGenerator`)
- if (thumbnail) this.uppy.removePlugin(thumbnail)
- }
- const plugins = this.opts.plugins || []
- plugins.forEach((pluginID) => {
- const plugin = this.uppy.getPlugin(pluginID)
- if (plugin) (plugin as any).unmount()
- })
- if (this.opts.theme === 'auto') {
- this.darkModeMediaQuery?.removeListener(this.handleSystemDarkModeChange)
- }
- if (this.opts.disablePageScrollWhenModalOpen) {
- document.body.classList.remove('uppy-Dashboard-isFixed')
- }
- this.unmount()
- this.removeEvents()
- }
- }
|