diff --git a/packages/devextreme/js/__internal/ui/file_uploader/file_uploader.ts b/packages/devextreme/js/__internal/ui/file_uploader/file_uploader.ts index 43613bddec55..e8c68b5965c5 100644 --- a/packages/devextreme/js/__internal/ui/file_uploader/file_uploader.ts +++ b/packages/devextreme/js/__internal/ui/file_uploader/file_uploader.ts @@ -144,6 +144,8 @@ class FileUploader extends Editor { _cancelButtonClickAction?: (event?: Partial) => void; + _fileLimitReachedAction?: () => void; + static __internals: { changeFileInputRenderer: (renderer: () => dxElementWrapper) => void; resetFileInputTag: () => void; @@ -211,6 +213,7 @@ class FileUploader extends Editor { onDropZoneEnter: null, onDropZoneLeave: null, onCancelButtonClick: null, + onFileLimitReached: undefined, allowedFileExtensions: [], maxFileSize: 0, minFileSize: 0, @@ -233,6 +236,7 @@ class FileUploader extends Editor { _hideCancelButtonOnUpload: true, _showFileIcon: false, _cancelButtonPosition: 'start', + _maxFileCount: undefined, }, }; } @@ -299,9 +303,11 @@ class FileUploader extends Editor { this._initFileInput(); this._initLabel(); - this._setUploadStrategy(); + + this._createFileLimitReachedAction(); this._createFiles(); + this._createBeforeSendAction(); this._createUploadStartedAction(); this._createUploadedAction(); @@ -336,11 +342,14 @@ class FileUploader extends Editor { if (!this._$fileInput) { this._$fileInput = renderFileUploaderInput(); - eventsEngine.on(this._$fileInput, 'change', this._inputChangeHandler.bind(this)); + eventsEngine.on(this._$fileInput, 'change', () => { this._inputChangeHandler(); }); eventsEngine.on(this._$fileInput, 'click', (e: Event): boolean | undefined => { e.stopPropagation(); + this._resetInputValue(); + const { useNativeInputClick } = this.option(); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return useNativeInputClick || this._isCustomClickEvent; }); @@ -374,13 +383,21 @@ class FileUploader extends Editor { const fileName = this._$fileInput.val().replace(/^.*\\/, ''); // @ts-expect-error dxElementWrapper should be extdened const files = this._$fileInput.prop('files'); + const { uploadMode } = this.option(); + if (files && !files.length && uploadMode !== 'useForm') { return; } + if (this._isFileLimitReached(files as unknown as File[])) { + this._fileLimitReachedAction?.(); + return; + } + // @ts-expect-error dxElementWrapper should be extdened const value = files ? this._getFiles(files) : [{ name: fileName }]; + this._changeValue(value as File[]); if (uploadMode === 'instantly') { @@ -388,6 +405,20 @@ class FileUploader extends Editor { } } + _isFileLimitReached(files: File[] = []): boolean { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { _maxFileCount, value } = this.option(); + + if (_maxFileCount === undefined) { + return false; + } + + const totalCount = files.length + (value?.length ?? 0); + const isFileLimitReached = totalCount > _maxFileCount; + + return isFileLimitReached; + } + _shouldFileListBeExtended(): boolean { const { uploadMode, extendSelection, multiple } = this.option(); @@ -398,6 +429,7 @@ class FileUploader extends Editor { const { value: currentValue } = this.option(); const files = this._shouldFileListBeExtended() ? currentValue?.slice() : []; + this.option({ value: files?.concat(value) }); } @@ -494,6 +526,10 @@ class FileUploader extends Editor { _createFiles(): void { const { value: files } = this.option(); + if (this._isFileLimitReached()) { + this._fileLimitReachedAction?.(); + } + if (this._files && (files?.length === 0 || !this._shouldFileListBeExtended())) { this._preventFilesUploading(this._files); this._files = null; @@ -605,6 +641,10 @@ class FileUploader extends Editor { this._cancelButtonClickAction = this._createActionByOption('onCancelButtonClick', { excludeValidators: ['readOnly'] }); } + _createFileLimitReachedAction(): void { + this._fileLimitReachedAction = this._createActionByOption('onFileLimitReached', { excludeValidators: ['readOnly'] }); + } + _createFile(value: File): FileUploaderItem { return { value, @@ -1230,13 +1270,19 @@ class FileUploader extends Editor { const fileList = e.originalEvent.dataTransfer.files; const files = this._getFiles(fileList); - const { multiple } = this.option(); + const { multiple, uploadMode } = this.option(); + if ((!multiple && files.length > 1) || files.length === 0) { return; } + if (this._isFileLimitReached(files as unknown as File[])) { + this._fileLimitReachedAction?.(); + return; + } + this._changeValue(files); - const { uploadMode } = this.option(); + if (uploadMode === 'instantly') { this._uploadFiles(); } @@ -1618,6 +1664,8 @@ class FileUploader extends Editor { case '_showFileIcon': this._invalidate(); break; + case '_maxFileCount': + break; case 'labelText': this._updateInputLabelText(); break; @@ -1678,6 +1726,9 @@ class FileUploader extends Editor { case 'onCancelButtonClick': this._createCancelButtonClickAction(); break; + case 'onFileLimitReached': + this._createFileLimitReachedAction(); + break; case 'useNativeInputClick': this._renderInput(); break; diff --git a/packages/devextreme/js/__internal/ui/file_uploader/file_uploader.types.ts b/packages/devextreme/js/__internal/ui/file_uploader/file_uploader.types.ts index 32e7a0d30294..db771f935bcd 100644 --- a/packages/devextreme/js/__internal/ui/file_uploader/file_uploader.types.ts +++ b/packages/devextreme/js/__internal/ui/file_uploader/file_uploader.types.ts @@ -118,6 +118,8 @@ export type CancelButtonClickEvent = NativeEventInfo & { readonly file?: File; }; +export type FileLimitReachedEvent = NativeEventInfo; + interface Properties extends PublicProperties { _buttonStylingMode?: ButtonStyle; @@ -129,6 +131,8 @@ interface Properties extends PublicProperties { _cancelButtonPosition?: 'start' | 'end'; + _maxFileCount?: number; + extendSelection?: boolean; allowCanceling?: boolean; @@ -139,7 +143,9 @@ interface Properties extends PublicProperties { useDragOver?: boolean; - onCancelButtonClick?: ((e: CancelButtonClickEvent) => void); + onCancelButtonClick?: (e: CancelButtonClickEvent) => void; + + onFileLimitReached?: (e: FileLimitReachedEvent) => void; } export interface FileUploaderProperties extends Properties, diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/fileUploader.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/fileUploader.tests.js index ff161f07f0b7..e391a92e0b65 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/fileUploader.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/fileUploader.tests.js @@ -4720,3 +4720,181 @@ QUnit.module('Accessibility', moduleConfig, () => { }); }); +QUnit.module('File limit', moduleConfig, () => { + QUnit.test('onFileLimitReached should be fired when selecting files exceeds the limit', function(assert) { + assert.expect(3); + + let fileLimitReachedCount = 0; + + const $element = $('#fileuploader').dxFileUploader({ + multiple: true, + _maxFileCount: 2, + onFileLimitReached: (e) => { + fileLimitReachedCount++; + + const { component, element } = e; + + assert.strictEqual(component, $element.dxFileUploader('instance'), 'component field is correct'); + assert.strictEqual($(element).is($element), true, 'element field is correct'); + }, + }); + + simulateFileChoose($element, [fakeFile, fakeFile1, fakeFile2]); + + assert.strictEqual(fileLimitReachedCount, 1, 'onFileLimitReached callback was called once'); + }); + + QUnit.test('files should not be added when limit is reached', function(assert) { + const $element = $('#fileuploader').dxFileUploader({ + multiple: true, + _maxFileCount: 2, + uploadMode: 'useButtons', + }); + const instance = $element.dxFileUploader('instance'); + + simulateFileChoose($element, [fakeFile, fakeFile1]); + assert.strictEqual(instance.option('value').length, 2, 'two files were added'); + + simulateFileChoose($element, [fakeFile2]); + assert.strictEqual(instance.option('value').length, 2, 'files count is still 2, third file was not added'); + }); + + QUnit.test('onFileLimitReached should be fired when drag and drop files exceeds the limit', function(assert) { + let fileLimitReachedCount = 0; + + const $element = $('#fileuploader').dxFileUploader({ + multiple: true, + _maxFileCount: 2, + uploadMode: 'useButtons', + onFileLimitReached: () => { + fileLimitReachedCount++; + }, + }); + + const instance = $element.dxFileUploader('instance'); + const $inputWrapper = $element.find(`.${FILEUPLOADER_INPUT_WRAPPER_CLASS}`); + + simulateFileChoose($element, [fakeFile]); + assert.strictEqual(instance.option('value').length, 1, 'one file was added'); + + const files = [fakeFile1, fakeFile2]; + + triggerDragEvent($inputWrapper, 'dragenter'); + triggerDragEvent($inputWrapper, 'drop', { files, types: ['Files'] }); + + assert.strictEqual(fileLimitReachedCount, 1, 'onFileLimitReached was fired'); + assert.strictEqual(instance.option('value').length, 1, 'files count is still 1'); + }); + + QUnit.test('files should be added when count is less than limit', function(assert) { + const $element = $('#fileuploader').dxFileUploader({ + multiple: true, + _maxFileCount: 3, + uploadMode: 'useButtons', + }); + const instance = $element.dxFileUploader('instance'); + + simulateFileChoose($element, [fakeFile, fakeFile1]); + assert.strictEqual(instance.option('value').length, 2, 'two files were added'); + + simulateFileChoose($element, [fakeFile2]); + assert.strictEqual(instance.option('value').length, 3, 'third file was added'); + }); + + QUnit.test('_maxFileCount: undefined should not limit files', function(assert) { + let fileLimitReachedCount = 0; + + const $element = $('#fileuploader').dxFileUploader({ + multiple: true, + _maxFileCount: undefined, + uploadMode: 'useButtons', + onFileLimitReached: () => { + fileLimitReachedCount++; + }, + }); + const instance = $element.dxFileUploader('instance'); + + simulateFileChoose($element, [fakeFile, fakeFile1, fakeFile2]); + + assert.strictEqual(fileLimitReachedCount, 0, 'onFileLimitReached was not called'); + assert.strictEqual(instance.option('value').length, 3, 'all three files were added'); + }); + + QUnit.test('onFileLimitReached should not be fired when _maxFileCount is not set', function(assert) { + let fileLimitReachedCount = 0; + + const $element = $('#fileuploader').dxFileUploader({ + multiple: true, + uploadMode: 'useButtons', + onFileLimitReached: () => { + fileLimitReachedCount++; + }, + }); + const instance = $element.dxFileUploader('instance'); + + simulateFileChoose($element, [fakeFile, fakeFile1, fakeFile2]); + + assert.strictEqual(fileLimitReachedCount, 0, 'onFileLimitReached was not called'); + assert.strictEqual(instance.option('value').length, 3, 'all files were added'); + }); + + QUnit.test('file limit should work in instantly upload mode', function(assert) { + let fileLimitReachedCount = 0; + + const $element = $('#fileuploader').dxFileUploader({ + multiple: true, + uploadMode: 'instantly', + _maxFileCount: 2, + onFileLimitReached: () => { + fileLimitReachedCount++; + }, + }); + const instance = $element.dxFileUploader('instance'); + + simulateFileChoose($element, [fakeFile, fakeFile1, fakeFile2]); + + assert.strictEqual(fileLimitReachedCount, 1, 'onFileLimitReached was called'); + assert.strictEqual(instance.option('value').length, 0, 'no files were added'); + }); + + QUnit.test('onFileLimitReached should be fired on initial render when value exceeds _maxFileCount', function(assert) { + let fileLimitReachedCount = 0; + + const $element = $('#fileuploader').dxFileUploader({ + multiple: true, + uploadMode: 'useButtons', + _maxFileCount: 2, + value: [fakeFile, fakeFile1, fakeFile2], + onFileLimitReached: () => { + fileLimitReachedCount++; + }, + }); + const instance = $element.dxFileUploader('instance'); + + assert.strictEqual(fileLimitReachedCount, 1, 'onFileLimitReached was called on init'); + assert.strictEqual(instance.option('value').length, 3, 'value won\'t reset to empty array'); + }); + + QUnit.test('onFileLimitReached should be fired when value is changed programmatically and exceeds _maxFileCount', function(assert) { + let fileLimitReachedCount = 0; + + const $element = $('#fileuploader').dxFileUploader({ + multiple: true, + uploadMode: 'useButtons', + _maxFileCount: 2, + onFileLimitReached: () => { + fileLimitReachedCount++; + }, + }); + const instance = $element.dxFileUploader('instance'); + + instance.option('value', [fakeFile, fakeFile1]); + assert.strictEqual(fileLimitReachedCount, 0, 'onFileLimitReached was not called for 2 files'); + assert.strictEqual(instance.option('value').length, 2, 'two files were set'); + + instance.option('value', [fakeFile, fakeFile1, fakeFile2]); + assert.strictEqual(fileLimitReachedCount, 1, 'onFileLimitReached was called when setting 3 files'); + assert.strictEqual(instance.option('value').length, 3, 'value is changed'); + }); +}); +