123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446 |
- 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'
- 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
- }
- 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
- /** @deprecated use option autoOpen instead */
- autoOpenFileEditor?: boolean
- defaultPickerIcon?: typeof defaultPickerIcon
- disabled?: boolean
- disableInformer?: boolean
- disableLocalFiles?: boolean
- disableStatusBar?: boolean
- disableThumbnailGenerator?: boolean
- doneButtonHandler?: () => 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
- 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,
- 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<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
- >
- private removeDragOverClassTimeout!: ReturnType<typeof setTimeout>
- constructor(uppy: Uppy<M, B>, opts?: DashboardOptions<M, B>) {
- // support for the legacy `autoOpenFileEditor` option,
- // TODO: we can remove this code when we update the Uppy major version
- let autoOpen: DashboardOptions<M, B>['autoOpen']
- if (!opts) {
- autoOpen = null
- } else if (opts.autoOpen === undefined) {
- autoOpen = opts.autoOpenFileEditor ? 'imageEditor' : null
- } else {
- autoOpen = opts.autoOpen
- }
- super(uppy, { ...defaultOptions, ...opts, autoOpen })
- this.id = this.opts.id || 'Dashboard'
- this.title = 'Dashboard'
- this.type = 'orchestrator'
- this.defaultLocale = locale
- // Dynamic default options:
- this.opts.doneButtonHandler ??= () => {
- this.uppy.clearUploadedFiles()
- 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
- clearTimeout(this.removeDragOverClassTimeout)
- 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
- clearTimeout(this.removeDragOverClassTimeout)
- this.setPluginState({ isDraggingOver: true })
- this.opts.onDragOver?.(event)
- }
- private handleDragLeave = (event: DragEvent) => {
- event.preventDefault()
- event.stopPropagation()
- clearTimeout(this.removeDragOverClassTimeout)
- // Timeout against flickering, this solution is taken from drag-drop library.
- // Solution with 'pointer-events: none' didn't work across browsers.
- this.removeDragOverClassTimeout = setTimeout(() => {
- this.setPluginState({ isDraggingOver: false })
- }, 50)
- this.opts.onDragLeave?.(event)
- }
- private handleDrop = async (event: DragEvent) => {
- event.preventDefault()
- event.stopPropagation()
- clearTimeout(this.removeDragOverClassTimeout)
- 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>): JSX.Element => {
- const pluginState = this.getPluginState()
- const { files, capabilities, allowNewUpload } = state
- const {
- newFiles,
- uploadStartedFiles,
- completeFiles,
- erroredFiles,
- inProgressFiles,
- inProgressNotPausedFiles,
- processingFiles,
- isUploadStarted,
- isAllComplete,
- isAllErrored,
- 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,
- isAllErrored,
- 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,
- isTargetDOMEl: this.isTargetDOMEl,
- 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)
- }
- }
- }
- 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.id}:StatusBar`,
- target: this,
- hideUploadButton: this.opts.hideUploadButton,
- hideRetryButton: this.opts.hideRetryButton,
- hidePauseResumeButton: this.opts.hidePauseResumeButton,
- hideCancelButton: this.opts.hideCancelButton,
- showProgressDetails: this.opts.showProgressDetails,
- hideAfterFinish: this.opts.hideProgressAfterFinish,
- locale: this.opts.locale,
- doneButtonHandler: this.opts.doneButtonHandler,
- })
- }
- if (!this.opts.disableInformer) {
- this.uppy.use(Informer, {
- id: `${this.id}:Informer`,
- target: this,
- })
- }
- if (!this.opts.disableThumbnailGenerator) {
- this.uppy.use(ThumbnailGenerator, {
- id: `${this.id}:ThumbnailGenerator`,
- thumbnailWidth: this.opts.thumbnailWidth,
- thumbnailHeight: this.opts.thumbnailHeight,
- thumbnailType: this.opts.thumbnailType,
- waitForThumbnailsBeforeUpload: this.opts.waitForThumbnailsBeforeUpload,
- // If we don't block on thumbnails, we can lazily generate them
- lazy: !this.opts.waitForThumbnailsBeforeUpload,
- })
- }
- // 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()
- }
- }
|