UNPKG

@uppy/dashboard

Version:

Universal UI plugin for Uppy.

1,152 lines (1,130 loc) 48.3 kB
function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } var id = 0; function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; } import { UIPlugin } from '@uppy/core'; 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.js"; import createSuperFocus from "./utils/createSuperFocus.js"; import DashboardUI from "./components/Dashboard.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json const packageJson = { "version": "3.8.0" }; import locale from "./locale.js"; const memoize = memoizeOne.default || memoizeOne; const TAB_KEY = 9; const ESC_KEY = 27; function createPromise() { const o = {}; o.promise = new Promise((resolve, reject) => { o.resolve = resolve; o.reject = reject; }); return o; } // set default options, must be kept in sync with packages/@uppy/react/src/DashboardModal.js const defaultOptions = { target: 'body', metaFields: [], inline: false, width: 750, height: 550, thumbnailWidth: 280, thumbnailType: 'image/jpeg', waitForThumbnailsBeforeUpload: false, defaultPickerIcon, showLinkToFileUploadResult: false, showProgressDetails: false, hideUploadButton: false, hideCancelButton: false, hideRetryButton: false, hidePauseResumeButton: false, hideProgressAfterFinish: false, note: null, closeModalOnClickOutside: false, closeAfterFinish: false, singleFileFullScreen: true, disableStatusBar: false, disableInformer: false, disableThumbnailGenerator: false, disablePageScrollWhenModalOpen: true, animateOpenClose: true, fileManagerSelectionType: 'files', proudlyDisplayPoweredByUppy: true, showSelectedFiles: true, showRemoveButtonAfterComplete: false, browserBackButtonClose: false, showNativePhotoCameraButton: false, showNativeVideoCameraButton: false, theme: 'light', 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, onRequestCloseModal: null }; /** * Dashboard UI with previews, metadata editing, tabs for various services and more */ var _disabledNodes = /*#__PURE__*/_classPrivateFieldLooseKey("disabledNodes"); var _generateLargeThumbnailIfSingleFile = /*#__PURE__*/_classPrivateFieldLooseKey("generateLargeThumbnailIfSingleFile"); var _openFileEditorWhenFilesAdded = /*#__PURE__*/_classPrivateFieldLooseKey("openFileEditorWhenFilesAdded"); var _attachRenderFunctionToTarget = /*#__PURE__*/_classPrivateFieldLooseKey("attachRenderFunctionToTarget"); var _isTargetSupported = /*#__PURE__*/_classPrivateFieldLooseKey("isTargetSupported"); var _getAcquirers = /*#__PURE__*/_classPrivateFieldLooseKey("getAcquirers"); var _getProgressIndicators = /*#__PURE__*/_classPrivateFieldLooseKey("getProgressIndicators"); var _getEditors = /*#__PURE__*/_classPrivateFieldLooseKey("getEditors"); var _addSpecifiedPluginsFromOptions = /*#__PURE__*/_classPrivateFieldLooseKey("addSpecifiedPluginsFromOptions"); var _autoDiscoverPlugins = /*#__PURE__*/_classPrivateFieldLooseKey("autoDiscoverPlugins"); var _addSupportedPluginIfNoTarget = /*#__PURE__*/_classPrivateFieldLooseKey("addSupportedPluginIfNoTarget"); export default class Dashboard extends UIPlugin { // Timeouts constructor(uppy, _opts) { var _this$opts4, _this$opts4$doneButto, _this$opts5, _this$opts5$onRequest; // support for the legacy `autoOpenFileEditor` option, // TODO: we can remove this code when we update the Uppy major version let autoOpen; if (!_opts) { autoOpen = null; } else if (_opts.autoOpen === undefined) { autoOpen = _opts.autoOpenFileEditor ? 'imageEditor' : null; } else { autoOpen = _opts.autoOpen; } super(uppy, { ...defaultOptions, ..._opts, autoOpen }); Object.defineProperty(this, _disabledNodes, { writable: true, value: void 0 }); this.modalName = `uppy-Dashboard-${nanoid()}`; this.superFocus = createSuperFocus(); this.ifFocusedOnUppyRecently = false; this.removeTarget = plugin => { 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 }); }; this.addTarget = plugin => { const callerPluginId = plugin.id || plugin.constructor.name; const callerPluginName = plugin.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; }; this.hideAllPanels = () => { var _state$activePickerPa; 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$activePickerPa = state.activePickerPanel) == null ? void 0 : _state$activePickerPa.id); }; this.showPanel = id => { 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); }; this.canEditFile = file => { const { targets } = this.getPluginState(); const editors = _classPrivateFieldLooseBase(this, _getEditors)[_getEditors](targets); return editors.some(target => this.uppy.getPlugin(target.id).canEditFile(file)); }; this.openFileEditor = file => { const { targets } = this.getPluginState(); const editors = _classPrivateFieldLooseBase(this, _getEditors)[_getEditors](targets); this.setPluginState({ showFileEditor: true, fileCardFor: file.id || null, activeOverlayType: 'FileEditor' }); editors.forEach(editor => { ; this.uppy.getPlugin(editor.id).selectFile(file); }); }; this.closeFileEditor = () => { 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' }); } }; this.saveFileEditor = () => { const { targets } = this.getPluginState(); const editors = _classPrivateFieldLooseBase(this, _getEditors)[_getEditors](targets); editors.forEach(editor => { ; this.uppy.getPlugin(editor.id).save(); }); this.closeFileEditor(); }; this.openModal = () => { const { promise, resolve } = createPromise(); // save scroll position this.savedScrollPosition = window.pageYOffset; // save active element, so we can restore focus when modal is closed this.savedActiveElement = document.activeElement; 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; }; this.closeModal = opts => { var _opts$manualClose; // Whether the modal is being closed by the user (`true`) or by other means (e.g. browser back button) const manualClose = (_opts$manualClose = opts == null ? void 0 : opts.manualClose) != null ? _opts$manualClose : true; const { isHidden, isClosing } = this.getPluginState(); if (isHidden || isClosing) { // short-circuit if animation is ongoing return undefined; } const { promise, resolve } = createPromise(); 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) { var _history$state; // Make sure that the latest entry in the history state is our modal name // eslint-disable-next-line no-restricted-globals if ((_history$state = history.state) != null && _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; }; this.isModalOpen = () => { return !this.getPluginState().isHidden || false; }; this.requestCloseModal = () => { if (this.opts.onRequestCloseModal) { return this.opts.onRequestCloseModal(); } return this.closeModal(); }; this.setDarkModeCapability = isDarkModeOn => { const { capabilities } = this.uppy.getState(); this.uppy.setState({ capabilities: { ...capabilities, darkMode: isDarkModeOn } }); }; this.handleSystemDarkModeChange = event => { const isDarkModeOnNow = event.matches; this.uppy.log(`[Dashboard] Dark mode is ${isDarkModeOnNow ? 'on' : 'off'}`); this.setDarkModeCapability(isDarkModeOnNow); }; this.toggleFileCard = (show, fileID) => { 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 }); }; this.toggleAddFilesPanel = show => { this.setPluginState({ showAddFilesPanel: show, activeOverlayType: show ? 'AddFiles' : null }); }; this.addFiles = files => { 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.relativePath || file.webkitRelativePath || null } })); try { this.uppy.addFiles(descriptors); } catch (err) { this.uppy.log(err); } }; // ___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. this.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); }; this.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. this.recordIfFocusedOnUppyRecently = event => { if (this.el.contains(event.target)) { 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(); } }; this.disableInteractiveElements = disable => { var _classPrivateFieldLoo; const NODES_TO_DISABLE = ['a[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', '[role="button"]:not([disabled])']; const nodesToDisable = (_classPrivateFieldLoo = _classPrivateFieldLooseBase(this, _disabledNodes)[_disabledNodes]) != null ? _classPrivateFieldLoo : toArray(this.el.querySelectorAll(NODES_TO_DISABLE)).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) { _classPrivateFieldLooseBase(this, _disabledNodes)[_disabledNodes] = nodesToDisable; } else { _classPrivateFieldLooseBase(this, _disabledNodes)[_disabledNodes] = null; } this.dashboardIsDisabled = disable; }; this.updateBrowserHistory = () => { var _history$state2; // Ensure history state does not already contain our modal name to avoid double-pushing // eslint-disable-next-line no-restricted-globals if (!((_history$state2 = history.state) != null && _history$state2[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); }; this.handlePopState = event => { var _event$state; // 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 = event.state) != null && _event$state[this.modalName]) { // eslint-disable-next-line no-restricted-globals history.back(); } }; this.handleKeyDownInModal = event => { // 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); }; this.handleClickOutside = () => { if (this.opts.closeModalOnClickOutside) this.requestCloseModal(); }; this.handlePaste = event => { // Let any acquirer plugin (Url/Webcam/etc.) handle pastes to the root this.uppy.iteratePlugins(plugin => { if (plugin.type === 'acquirer') { // Every Plugin with .type acquirer can define handleRootPaste(event) ; plugin.handleRootPaste == null || plugin.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); } }; this.handleInputChange = event => { event.preventDefault(); const files = toArray(event.target.files); if (files.length > 0) { this.uppy.log('[Dashboard] Files selected through input'); this.addFiles(files); } }; this.handleDragOver = event => { var _this$opts$onDragOver, _this$opts; 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.canHandleRootDrop != null && plugin.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 = (_this$opts = this.opts).onDragOver) == null || _this$opts$onDragOver.call(_this$opts, event); }; this.handleDragLeave = event => { var _this$opts$onDragLeav, _this$opts2; 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$onDragLeav = (_this$opts2 = this.opts).onDragLeave) == null || _this$opts$onDragLeav.call(_this$opts2, event); }; this.handleDrop = async event => { var _this$opts$onDrop, _this$opts3; 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.handleRootDrop == null || plugin.handleRootDrop(event); } }); // Add all dropped files let executedDropErrorOnce = false; const logDropError = error => { 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 = (_this$opts3 = this.opts).onDrop) == null || _this$opts$onDrop.call(_this$opts3, event); }; this.handleRequestThumbnail = file => { 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. */ this.handleCancelThumbnail = file => { if (!this.opts.waitForThumbnailsBeforeUpload) { this.uppy.emit('thumbnail:cancel', file); } }; this.handleKeyDownInInline = event => { // 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. this.handlePasteOnBody = event => { const isFocusInOverlay = this.el.contains(document.activeElement); if (isFocusInOverlay) { this.handlePaste(event); } }; this.handleComplete = _ref => { let { failed } = _ref; if (this.opts.closeAfterFinish && !(failed != null && failed.length)) { // All uploads are done this.requestCloseModal(); } }; this.handleCancelRestore = () => { this.uppy.emit('restore-canceled'); }; Object.defineProperty(this, _generateLargeThumbnailIfSingleFile, { writable: true, value: () => { 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`); thumbnailGenerator == null || thumbnailGenerator.setOptions({ thumbnailWidth: LARGE_THUMBNAIL }); const fileForThumbnail = { ...files[0], preview: undefined }; thumbnailGenerator == null || thumbnailGenerator.requestThumbnail(fileForThumbnail).then(() => { thumbnailGenerator == null || thumbnailGenerator.setOptions({ thumbnailWidth: this.opts.thumbnailWidth }); }); } } }); Object.defineProperty(this, _openFileEditorWhenFilesAdded, { writable: true, value: files => { 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); } } }); this.initEvents = () => { // 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', _classPrivateFieldLooseBase(this, _addSupportedPluginIfNoTarget)[_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', _classPrivateFieldLooseBase(this, _generateLargeThumbnailIfSingleFile)[_generateLargeThumbnailIfSingleFile]); this.uppy.on('file-removed', _classPrivateFieldLooseBase(this, _generateLargeThumbnailIfSingleFile)[_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', _classPrivateFieldLooseBase(this, _openFileEditorWhenFilesAdded)[_openFileEditorWhenFilesAdded]); } }; this.removeEvents = () => { 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', _classPrivateFieldLooseBase(this, _addSupportedPluginIfNoTarget)[_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', _classPrivateFieldLooseBase(this, _generateLargeThumbnailIfSingleFile)[_generateLargeThumbnailIfSingleFile]); this.uppy.off('file-removed', _classPrivateFieldLooseBase(this, _generateLargeThumbnailIfSingleFile)[_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', _classPrivateFieldLooseBase(this, _openFileEditorWhenFilesAdded)[_openFileEditorWhenFilesAdded]); } }; this.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(); } }; this.afterUpdate = () => { if (this.opts.disabled && !this.dashboardIsDisabled) { this.disableInteractiveElements(true); return; } if (!this.opts.disabled && this.dashboardIsDisabled) { this.disableInteractiveElements(false); } this.superFocusOnEachUpdate(); }; this.saveFileCard = (meta, fileID) => { this.uppy.setFileMeta(fileID, meta); this.toggleFileCard(false, fileID); }; Object.defineProperty(this, _attachRenderFunctionToTarget, { writable: true, value: target => { const plugin = this.uppy.getPlugin(target.id); return { ...target, icon: plugin.icon || this.opts.defaultPickerIcon, render: plugin.render }; } }); Object.defineProperty(this, _isTargetSupported, { writable: true, value: target => { const plugin = this.uppy.getPlugin(target.id); // If the plugin does not provide a `supported` check, assume the plugin works everywhere. if (typeof plugin.isSupported !== 'function') { return true; } return plugin.isSupported(); } }); Object.defineProperty(this, _getAcquirers, { writable: true, value: memoize(targets => { return targets.filter(target => target.type === 'acquirer' && _classPrivateFieldLooseBase(this, _isTargetSupported)[_isTargetSupported](target)).map(_classPrivateFieldLooseBase(this, _attachRenderFunctionToTarget)[_attachRenderFunctionToTarget]); }) }); Object.defineProperty(this, _getProgressIndicators, { writable: true, value: memoize(targets => { return targets.filter(target => target.type === 'progressindicator').map(_classPrivateFieldLooseBase(this, _attachRenderFunctionToTarget)[_attachRenderFunctionToTarget]); }) }); Object.defineProperty(this, _getEditors, { writable: true, value: memoize(targets => { return targets.filter(target => target.type === 'editor').map(_classPrivateFieldLooseBase(this, _attachRenderFunctionToTarget)[_attachRenderFunctionToTarget]); }) }); this.render = state => { 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 = _classPrivateFieldLooseBase(this, _getAcquirers)[_getAcquirers](pluginState.targets); const progressindicators = _classPrivateFieldLooseBase(this, _getProgressIndicators)[_getProgressIndicators](pluginState.targets); const editors = _classPrivateFieldLooseBase(this, _getEditors)[_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 }); }; Object.defineProperty(this, _addSpecifiedPluginsFromOptions, { writable: true, value: () => { const plugins = this.opts.plugins || []; plugins.forEach(pluginID => { const plugin = this.uppy.getPlugin(pluginID); if (plugin) { ; plugin.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'); } }); } }); Object.defineProperty(this, _autoDiscoverPlugins, { writable: true, value: () => { this.uppy.iteratePlugins(_classPrivateFieldLooseBase(this, _addSupportedPluginIfNoTarget)[_addSupportedPluginIfNoTarget]); } }); Object.defineProperty(this, _addSupportedPluginIfNoTarget, { writable: true, value: plugin => { var _plugin$opts; // 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 = plugin.opts) != null && _plugin$opts.target) && typesAllowed.includes(plugin.type)) { const pluginAlreadyAdded = this.getPluginState().targets.some(installedPlugin => plugin.id === installedPlugin.id); if (!pluginAlreadyAdded) { ; plugin.mount(this, plugin); } } } }); this.install = () => { // 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') { var _this$darkModeMediaQu; (_this$darkModeMediaQu = this.darkModeMediaQuery) == null || _this$darkModeMediaQu.addListener(this.handleSystemDarkModeChange); } _classPrivateFieldLooseBase(this, _addSpecifiedPluginsFromOptions)[_addSpecifiedPluginsFromOptions](); _classPrivateFieldLooseBase(this, _autoDiscoverPlugins)[_autoDiscoverPlugins](); this.initEvents(); }; this.uninstall = () => { 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.unmount(); }); if (this.opts.theme === 'auto') { var _this$darkModeMediaQu2; (_this$darkModeMediaQu2 = this.darkModeMediaQuery) == null || _this$darkModeMediaQu2.removeListener(this.handleSystemDarkModeChange); } if (this.opts.disablePageScrollWhenModalOpen) { document.body.classList.remove('uppy-Dashboard-isFixed'); } this.unmount(); this.removeEvents(); }; this.id = this.opts.id || 'Dashboard'; this.title = 'Dashboard'; this.type = 'orchestrator'; this.defaultLocale = locale; // Dynamic default options: (_this$opts4$doneButto = (_this$opts4 = this.opts).doneButtonHandler) != null ? _this$opts4$doneButto : _this$opts4.doneButtonHandler = () => { this.uppy.clearUploadedFiles(); this.requestCloseModal(); }; (_this$opts5$onRequest = (_this$opts5 = this.opts).onRequestCloseModal) != null ? _this$opts5$onRequest : _this$opts5.onRequestCloseModal = () => this.closeModal(); this.i18nInit(); } } Dashboard.VERSION = packageJson.version;