UNPKG

@progress/kendo-angular-upload

Version:

Kendo UI Angular Upload Component

1,576 lines (1,554 loc) 201 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import * as i1 from '@angular/common/http'; import { HttpHeaders, HttpRequest, HttpEventType, HttpResponse } from '@angular/common/http'; import * as i0 from '@angular/core'; import { EventEmitter, Injectable, Directive, ElementRef, ContentChild, ViewChild, Input, HostBinding, Output, Component, HostListener, ViewChildren, Inject, forwardRef, isDevMode, NgModule } from '@angular/core'; import { guid, Keys, isControlRequired, isChanged, isDocumentAvailable, KendoInput, ResizeBatchService } from '@progress/kendo-angular-common'; import { fileAudioIcon, fileVideoIcon, fileImageIcon, fileTxtIcon, filePresentationIcon, fileDataIcon, fileProgrammingIcon, filePdfIcon, fileConfigIcon, fileZipIcon, fileDiscImageIcon, arrowRotateCwSmallIcon, playSmIcon, pauseSmIcon, cancelIcon, xIcon, copyIcon, fileIcon, checkIcon, exclamationCircleIcon, uploadIcon } from '@progress/kendo-svg-icons'; import { NgControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import * as i1$1 from '@progress/kendo-angular-l10n'; import { ComponentMessages, LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n'; import { fromEvent, merge } from 'rxjs'; import { filter } from 'rxjs/operators'; import { validatePackage } from '@progress/kendo-licensing'; import { trigger, state, style, transition, animate } from '@angular/animations'; import { ButtonComponent } from '@progress/kendo-angular-buttons'; import { NgIf, NgFor, NgClass, NgTemplateOutlet } from '@angular/common'; import { IconWrapperComponent, IconsService } from '@progress/kendo-angular-icons'; import { ProgressBarComponent } from '@progress/kendo-angular-progressbar'; import { PopupService } from '@progress/kendo-angular-popup'; /** * Lists the possible states of a file. */ var FileState; (function (FileState) { /** * The file upload process failed. */ FileState[FileState["Failed"] = 0] = "Failed"; /** * An initially selected file without a set state. */ FileState[FileState["Initial"] = 1] = "Initial"; /** * The file is selected. */ FileState[FileState["Selected"] = 2] = "Selected"; /** * The file uploaded successfully. */ FileState[FileState["Uploaded"] = 3] = "Uploaded"; /** * The file is uploading. */ FileState[FileState["Uploading"] = 4] = "Uploading"; /** * The file upload process is paused. */ FileState[FileState["Paused"] = 5] = "Paused"; })(FileState || (FileState = {})); /** * @hidden */ class FileMap { _files; constructor() { this._files = {}; } add(file) { const uid = file.uid; if (this.has(uid)) { if (file.validationErrors && file.validationErrors.length > 0) { this._files[uid].unshift(file); } else { this._files[uid].push(file); } } else { this._files[uid] = [file]; } } remove(uid) { if (this.has(uid)) { this._files[uid] = null; delete this._files[uid]; } } clear() { const allFiles = this._files; for (const uid in allFiles) { if (allFiles.hasOwnProperty(uid)) { for (const file of allFiles[uid]) { if (file.httpSubscription) { file.httpSubscription.unsubscribe(); } } allFiles[uid] = null; delete allFiles[uid]; } } } has(uid) { return uid in this._files; } get(uid) { return this._files[uid]; } setFilesState(files, state) { for (const file of files) { this.setFilesStateByUid(file.uid, state); } } setFilesStateByUid(uid, state) { this.get(uid).forEach((f) => { f.state = state; }); } get count() { return Object.getOwnPropertyNames(this._files).length; } get files() { const initial = this._files; const transformed = []; for (const uid in initial) { if (initial.hasOwnProperty(uid)) { transformed.push(initial[uid]); } } return transformed; } get filesFlat() { const initial = this._files; const transformed = []; for (const uid in initial) { if (initial.hasOwnProperty(uid)) { const current = initial[uid]; current.forEach((file) => { transformed.push(file); }); } } return transformed; } get filesToUpload() { const files = this._files; const notUploaded = []; for (const uid in files) { if (files.hasOwnProperty(uid)) { const currentFiles = files[uid]; let currentFilesValid = true; for (const file of currentFiles) { if (file.state !== FileState.Selected || (file.validationErrors && file.validationErrors.length > 0)) { currentFilesValid = false; } } if (currentFilesValid) { notUploaded.push(currentFiles); } } } return notUploaded; } get firstFileToUpload() { const files = this._files; for (const uid in files) { if (files.hasOwnProperty(uid)) { const currentFiles = files[uid]; let currentFilesValid = true; for (const file of currentFiles) { if (file.state !== FileState.Selected || (file.validationErrors && file.validationErrors.length > 0)) { currentFilesValid = false; } } if (currentFilesValid) { return currentFiles; } } } return null; } getFilesWithState(state) { return this.filesFlat.filter(file => file.state === state); } hasFileWithState(fileStates) { const files = this._files; for (const uid in files) { if (files.hasOwnProperty(uid)) { const currentFiles = files[uid]; for (const file of currentFiles) { if (fileStates.indexOf(file.state) >= 0) { return true; } } } } return false; } } /** * Arguments for the `cancel` event. The `cancel` event fires when * you cancel the upload of a file or batch of files. * * ```typescript * @Component({ * template: ` * <kendo-upload (cancel)="cancelEventHandler($event)"></kendo-upload> * ` * }) * export class UploadComponent { * public cancelEventHandler(e: CancelEvent) { * console.log('Canceling file upload', e.files); * } * } * ``` */ class CancelEvent { /** * The files that you canceled during the upload process. */ files; /** * @hidden * Constructs the event arguments for the `cancel` event. * @param files - The list of the files that were going to be uploaded. */ constructor(files) { this.files = files; } } /** * @hidden */ class PreventableEvent { prevented = false; /** * Prevents the default action for a specified event. * In this way, the source component suppresses the built-in behavior that follows the event. */ preventDefault() { this.prevented = true; } /** * If the event is prevented by any of its subscribers, returns `true`. * * @returns `true` if the default action was prevented. Otherwise, returns `false`. */ isDefaultPrevented() { return this.prevented; } } /** * Arguments for the `clear` event. The `clear` event fires when * the **Clear** button is clicked and selected files are about to be cleared. * * ```typescript * @Component({ * template: ` * <kendo-upload (clear)="clearEventHandler($event)"></kendo-upload> * ` * }) * export class UploadComponent { * public clearEventHandler(e: ClearEvent) { * console.log('Clearing the file upload'); * } * } * ``` */ class ClearEvent extends PreventableEvent { /** * @hidden * Constructs the event arguments for the `clear` event. */ constructor() { super(); } } /** * Arguments for the `error` event. The `error` event fires when * an `upload` or `remove` operation fails. * * ```typescript * @Component({ * template: ` * <kendo-upload (error)="errorEventHandler($event)"></kendo-upload> * ` * }) * export class UploadComponent { * public errorEventHandler(e: ErrorEvent) { * console.log('An error occurred'); * } * } * ``` */ class ErrorEvent { /** * The array of files that failed to be uploaded or removed. */ files; /** * The operation type that failed (`upload` or `remove`). */ operation; /** * The HTTP response returned by the server containing error details. */ response; /** * @hidden * Constructs the event arguments for the `error` event. * * @param files - The list of the files that failed to be uploaded or removed. * @param operation - The operation type (`upload` or `remove`). * @param response - The response object returned by the server. */ constructor(files, operation, response) { this.files = files; this.operation = operation; this.response = response; } } /** * Arguments for the `pause` event. The `pause` event fires when * you pause a file that is currently uploading. * * ```typescript * @Component({ * template: ` * <kendo-upload * [chunkable]="true" * (pause)="pauseEventHandler($event)"> * </kendo-upload> * ` * }) * export class UploadComponent { * public pauseEventHandler(ev: PauseEvent) { * console.log('File paused'); * } * } * ``` */ class PauseEvent { /** * The file that you paused during the upload process. */ file; /** * @hidden * Constructs the event arguments for the `pause` event. * @param file - The file that is going to be paused. */ constructor(file) { this.file = file; } } /** * Arguments for the `remove` event. The `remove` event fires when you are about to remove an uploaded * or selected file. You can cancel this event to prevent removal. * * ```typescript * @Component({ * template: ` * <kendo-upload (remove)="removeEventHandler($event)"></kendo-upload> * ` * }) * export class UploadComponent { * public removeEventHandler(e: RemoveEvent) { * console.log('Removing a file'); * } * } * ``` */ class RemoveEvent extends PreventableEvent { /** * An optional object that you send to the `remove` handler as key/value pairs. * */ data; /** * The files that you will remove from the server. */ files; /** * The headers of the request. * You can use this to add custom headers to the remove request. */ headers; /** * @hidden * Constructs the event arguments for the `remove` event. * @param files - The list of the files that will be removed. * @param headers - The headers of the request. */ constructor(files, headers) { super(); this.files = files; this.headers = headers; } } /** * Arguments for the `resume` event. The `resume` event fires when * you resume a previously paused file upload. * * ```typescript * @Component({ * template: ` * <kendo-upload * [chunkable]="true" * (resume)="resumeEventHandler($event)"> * </kendo-upload> * ` * }) * export class UploadComponent { * public resumeEventHandler(ev: ResumeEvent) { * console.log('File resumed'); * } * } * ``` */ class ResumeEvent { /** * The file that you resumed during the upload process. */ file; /** * @hidden * Constructs the event arguments for the `resume` event. * @param file - The file that is going to be resumed. */ constructor(file) { this.file = file; } } /** * Arguments for the `select` event. The `select` event fires when * a file or multiple files are selected for upload. The event can be canceled to prevent selection. * * ```typescript * @Component({ * template: ` * <kendo-upload (select)="selectEventHandler($event)"></kendo-upload> * ` * }) * export class UploadComponent { * public selectEventHandler(e: SelectEvent) { * console.log('File selected'); * } * } * ``` */ class SelectEvent extends PreventableEvent { /** * The files that are selected for upload. */ files; /** * @hidden * Constructs the event arguments for the `select` event. * @param files - The list of the selected files. */ constructor(files) { super(); this.files = files; } } /** * Arguments for the `success` event. The `success` event fires when * you successfully upload or remove the selected files. * * ```typescript * @Component({ * template: ` * <kendo-upload (success)="successEventHandler($event)"></kendo-upload> * ` * }) * export class UploadComponent { * public successEventHandler(e: SuccessEvent) { * console.log('The ' + e.operation + ' was successful!'); * } * } * ``` */ class SuccessEvent extends PreventableEvent { /** * The files that you successfully uploaded or removed. */ files; /** * The operation type that succeeded (`upload` or `remove`). */ operation; /** * The HTTP response that the server returns to confirm success. */ response; /** * @hidden * Constructs the event arguments for the `success` event. * @param files - The list of the files that were uploaded or removed. * @param operation - The operation type (`upload` or `remove`). * @param response - The response object returned by the server. */ constructor(files, operation, response) { super(); this.files = files; this.operation = operation; this.response = response; } } /** * Arguments for the `upload` event. The `upload` event fires when you are about * to upload one or more files. You can cancel this event to prevent upload and add headers to the request. * * ```typescript * @Component({ * template: ` * <kendo-upload (upload)="uploadEventHandler($event)"></kendo-upload> * ` * }) * export class UploadComponent { * public uploadEventHandler(e: UploadEvent) { * e.headers = e.headers.append('X-Foo', 'Bar'); * } * } * ``` */ class UploadEvent extends PreventableEvent { /** * The optional object that you send to the `upload` handler as key/value pairs. * */ data; /** * The files that you will upload to the server. */ files; /** * The headers of the request. * You can use this to add custom headers to the upload request. */ headers; /** * @hidden * Constructs the event arguments for the `upload` event. * @param files - The list of the files that will be uploaded. * @param headers - The headers of the request. */ constructor(files, headers) { super(); this.files = files; this.headers = headers; } } /** * Arguments for the `uploadprogress` event. The `uploadprogress` event * fires when you upload files. * * ```typescript * @Component({ * template: ` * <kendo-upload (uploadProgress)="uploadProgressEventHandler($event)"></kendo-upload> * ` * }) * export class UploadComponent { * public uploadProgressEventHandler(e: UploadProgressEvent) { * console.log(e.files[0].name + ' is ' + e.percentComplete + ' uploaded'); * } * } * ``` */ class UploadProgressEvent { /** * The files that you are currently uploading. */ files; /** * The upload progress as a percentage from 0 to 100. */ percentComplete; /** * @hidden * Constructs the event arguments for the `uploadprogress` event. * @param files - The list of files that are being uploaded. * @param percentComplete - The portion that has been uploaded. */ constructor(files, percentComplete) { this.files = files; this.percentComplete = percentComplete; } } /** * @hidden */ const fileGroupMap = { audio: [ ".aif", ".iff", ".m3u", ".m4a", ".mid", ".mp3", ".mpa", ".wav", ".wma", ".ogg", ".wav", ".wma", ".wpl" ], video: [ ".3g2", ".3gp", ".avi", ".asf", ".flv", ".m4u", ".rm", ".h264", ".m4v", ".mkv", ".mov", ".mp4", ".mpg", ".rm", ".swf", ".vob", ".wmv" ], image: [ ".ai", ".dds", ".heic", ".jpe", "jfif", ".jif", ".jp2", ".jps", ".eps", ".bmp", ".gif", ".jpeg", ".jpg", ".png", ".ps", ".psd", ".svg", ".svgz", ".tif", ".tiff" ], txt: [ ".doc", ".docx", ".log", ".pages", ".tex", ".wpd", ".wps", ".odt", ".rtf", ".text", ".txt", ".wks" ], presentation: [ ".key", ".odp", ".pps", ".ppt", ".pptx" ], data: [ ".xlr", ".xls", ".xlsx" ], programming: [ ".tmp", ".bak", ".msi", ".cab", ".cpl", ".cur", ".dll", ".dmp", ".drv", ".icns", ".ico", ".link", ".sys", ".cfg", ".ini", ".asp", ".aspx", ".cer", ".csr", ".css", ".dcr", ".htm", ".html", ".js", ".php", ".rss", ".xhtml" ], pdf: [ ".pdf" ], config: [ ".apk", ".app", ".bat", ".cgi", ".com", ".exe", ".gadget", ".jar", ".wsf" ], zip: [ ".7z", ".cbr", ".gz", ".sitx", ".arj", ".deb", ".pkg", ".rar", ".rpm", ".tar.gz", ".z", ".zip", ".zipx" ], discImage: [ ".dmg", ".iso", ".toast", ".vcd", ".bin", ".cue", ".mdf" ] }; /** * @hidden */ const fileSVGGroupMap = { audio: fileAudioIcon, video: fileVideoIcon, image: fileImageIcon, txt: fileTxtIcon, presentation: filePresentationIcon, data: fileDataIcon, programming: fileProgrammingIcon, pdf: filePdfIcon, config: fileConfigIcon, zip: fileZipIcon, discImage: fileDiscImageIcon }; /* eslint-disable no-bitwise */ /** * @hidden */ const getTotalFilesSizeMessage = (files) => { let totalSize = 0; let i; if (typeof files[0].size === "number") { for (i = 0; i < files.length; i++) { if (files[i].size) { totalSize += files[i].size; } } } else { return ""; } totalSize /= 1024; if (totalSize < 1024) { return totalSize.toFixed(2) + " KB"; } else { return (totalSize / 1024).toFixed(2) + " MB"; } }; const stripPath = (name) => { const slashIndex = name.lastIndexOf("\\"); return (slashIndex !== -1) ? name.substr(slashIndex + 1) : name; }; const getFileExtension = (fileName) => { const rFileExtension = /\.([^\.]+)$/; const matches = fileName.match(rFileExtension); return matches ? matches[0] : ""; }; /** * @hidden */ const validateInitialFileInfo = (file) => { if (file instanceof Object && file.hasOwnProperty("name")) { return true; } return false; }; /** * @hidden */ const validateInitialFileSelectFile = (file) => { if (file instanceof File || validateInitialFileInfo(file)) { return true; } return false; }; /** * @hidden */ const getInitialFileInfo = (fakeFile) => { fakeFile.extension = fakeFile.extension || getFileExtension(fakeFile.name); fakeFile.name = fakeFile.name; // eslint-disable-line no-self-assign fakeFile.size = fakeFile.size || 0; if (!fakeFile.hasOwnProperty("state")) { fakeFile.state = FileState.Initial; } if (!fakeFile.hasOwnProperty("uid")) { fakeFile.uid = guid(); } return fakeFile; }; /** * @hidden */ const convertFileToFileInfo = (file) => { const fileInfo = getFileInfo(file); fileInfo.uid = guid(); // Used to differentiate initial FileInfo objects and actual Files fileInfo.state = FileState.Selected; return fileInfo; }; const getFileInfo = (rawFile) => { const fileName = rawFile.name; const fileSize = rawFile.size; return { extension: getFileExtension(fileName), name: fileName, rawFile: rawFile, size: fileSize, state: FileState.Selected }; }; /** * @hidden */ const getAllFileInfo = (rawFiles) => { const allFileInfo = new Array(); let i; for (i = 0; i < rawFiles.length; i++) { allFileInfo.push(getFileInfo(rawFiles[i])); } return allFileInfo; }; /** * @hidden */ const fileHasValidationErrors = (file) => { if (file.validationErrors && file.validationErrors.length > 0) { return true; } return false; }; /** * @hidden */ const filesHaveValidationErrors = (files) => { for (const file of files) { if (fileHasValidationErrors(file)) { return true; } } return false; }; /** * @hidden */ const inputFiles = (input) => { if (input.files) { return getAllFileInfo(input.files); } else { //Required for testing const fileNames = input.value.split("|").map((file, index) => { const fileName = file.trim(); return { extension: getFileExtension(fileName), name: stripPath(fileName), rawFile: null, size: (index + 1) * 1000, state: FileState.Selected }; }); return fileNames; } }; /** * @hidden */ const assignGuidToFiles = (files, isUnique) => { const uid = guid(); return files.map((file) => { file.uid = isUnique ? guid() : uid; return file; }); }; /** * @hidden */ const supportsFormData = () => { return typeof (FormData) !== "undefined"; }; /** * @hidden */ const userAgent = () => { return navigator.userAgent; }; const focusableRegex = /^(?:a|input|select|textarea|button|object)$/i; /** * @hidden */ const IGNORE_TARGET_CLASSES = 'k-icon k-select k-input k-multiselect-wrap'; /** * @hidden */ const UPLOAD_CLASSES = 'k-upload-button k-clear-selected k-upload-selected k-upload-action k-file'; const isVisible = (element) => { const rect = element.getBoundingClientRect(); return !!(rect.width && rect.height) && window.getComputedStyle(element).visibility !== 'hidden'; }; const toClassList = (classNames) => String(classNames).trim().split(' '); /** * @hidden */ const hasClasses = (element, classNames) => { const namesList = toClassList(classNames); return Boolean(toClassList(element.className).find((className) => namesList.indexOf(className) >= 0)); }; /** * @hidden */ const isFocusable = (element, checkVisibility = true) => { if (element.tagName) { const tagName = element.tagName.toLowerCase(); const tabIndex = element.getAttribute('tabIndex'); const validTabIndex = tabIndex !== null && !isNaN(tabIndex) && tabIndex > -1; let focusable = false; if (focusableRegex.test(tagName)) { focusable = !element.disabled; } else { focusable = validTabIndex; } return focusable && (!checkVisibility || isVisible(element)); } return false; }; /** * @hidden */ const getFileGroupCssClass = (fileExtension) => { const initial = 'file'; for (const group in fileGroupMap) { if (fileGroupMap[group].indexOf(fileExtension) >= 0) { if (group === 'discImage') { return `${initial}-disc-image`; } return `${initial}-${group}`; } } return initial; }; /** * @hidden */ const isPresent = (value) => value !== null && value !== undefined; /** * @hidden */ class ChunkMap { _files; constructor() { this._files = {}; } add(uid, totalChunks) { const initialChunkInfo = { index: 0, position: 0, retries: 0, totalChunks: totalChunks }; this._files[uid] = initialChunkInfo; return initialChunkInfo; } remove(uid) { if (this.has(uid)) { this._files[uid] = null; delete this._files[uid]; } } has(uid) { return uid in this._files; } get(uid) { return this._files[uid]; } } /** * @hidden */ class UploadService { http; cancelEvent = new EventEmitter(); clearEvent = new EventEmitter(); completeEvent = new EventEmitter(); errorEvent = new EventEmitter(); pauseEvent = new EventEmitter(); removeEvent = new EventEmitter(); resumeEvent = new EventEmitter(); selectEvent = new EventEmitter(); successEvent = new EventEmitter(); uploadEvent = new EventEmitter(); uploadProgressEvent = new EventEmitter(); /** * Required for the `ControlValueAccessor` integration */ changeEvent = new EventEmitter(); /** * Default async settings */ async = { autoUpload: true, batch: false, chunk: false, concurrent: true, removeField: "fileNames", removeHeaders: new HttpHeaders(), removeMethod: "POST", removeUrl: "", responseType: "json", saveField: "files", saveHeaders: new HttpHeaders(), saveMethod: "POST", saveUrl: "", withCredentials: true }; /** * Default chunk settings */ chunk = { autoRetryAfter: 100, size: 1024 * 1024, maxAutoRetries: 1, resumable: true }; component = 'Upload'; chunkMap = new ChunkMap(); fileList = new FileMap(); constructor(http) { this.http = http; } get files() { return this.fileList; } setChunkSettings(settings) { if (settings !== false) { this.async.chunk = true; if (typeof settings === "object") { this.chunk = Object.assign({}, this.chunk, settings); } } } onChange() { const files = this.fileList.filesFlat.filter((file) => { return file.state === FileState.Initial || file.state === FileState.Uploaded; }); this.changeEvent.emit(files.length > 0 ? files : null); } addFiles(files) { const selectEventArgs = new SelectEvent(files); this.selectEvent.emit(selectEventArgs); if (!selectEventArgs.isDefaultPrevented()) { for (const file of files) { this.fileList.add(file); } if (this.async.autoUpload) { this.uploadFiles(); } } if (this.component === 'FileSelect') { const flatFiles = this.fileList.filesFlat; this.changeEvent.emit(flatFiles.length > 0 ? flatFiles : null); } } addInitialFiles(initialFiles) { this.fileList.clear(); initialFiles.forEach((file) => { const fakeFile = getInitialFileInfo(file); this.fileList.add(fakeFile); }); } addInitialFileSelectFiles(initialFiles) { this.fileList.clear(); initialFiles.forEach((file) => { if (file instanceof File) { this.fileList.add(convertFileToFileInfo(file)); } else { this.fileList.add(getInitialFileInfo(file)); } }); } resumeFile(uid) { const fileToResume = this.fileList.get(uid); this.resumeEvent.emit(new ResumeEvent(fileToResume[0])); this.fileList.setFilesStateByUid(uid, FileState.Uploading); this._uploadFiles([fileToResume]); } pauseFile(uid) { const pausedFile = this.fileList.get(uid)[0]; this.pauseEvent.emit(new PauseEvent(pausedFile)); this.fileList.setFilesStateByUid(uid, FileState.Paused); } removeFiles(uid) { const removedFiles = this.fileList.get(uid); // Clone the Headers so that the default ones are not overridden const removeEventArgs = new RemoveEvent(removedFiles, this.cloneRequestHeaders(this.async.removeHeaders)); this.removeEvent.emit(removeEventArgs); if (!removeEventArgs.isDefaultPrevented()) { if (this.component === 'Upload' && (removedFiles[0].state === FileState.Uploaded || removedFiles[0].state === FileState.Initial)) { this.performRemove(removedFiles, removeEventArgs); } else { this.fileList.remove(uid); if (this.component === 'FileSelect') { const flatFiles = this.fileList.filesFlat; this.changeEvent.emit(flatFiles.length > 0 ? flatFiles : null); } } } } cancelFiles(uid) { const canceledFiles = this.fileList.get(uid); const cancelEventArgs = new CancelEvent(canceledFiles); this.cancelEvent.emit(cancelEventArgs); for (const file of canceledFiles) { if (file.httpSubscription) { file.httpSubscription.unsubscribe(); } } this.fileList.remove(uid); this.checkAllComplete(); } clearFiles() { const clearEventArgs = new ClearEvent(); this.clearEvent.emit(clearEventArgs); if (!clearEventArgs.isDefaultPrevented()) { const triggerChange = this.fileList.hasFileWithState([ FileState.Initial, FileState.Uploaded ]); this.fileList.clear(); if (triggerChange) { this.onChange(); } } } uploadFiles() { let filesToUpload = []; if (this.async.concurrent) { filesToUpload = this.fileList.filesToUpload; } if (!this.async.concurrent && !this.fileList.hasFileWithState([FileState.Uploading])) { filesToUpload = this.fileList.firstFileToUpload ? [this.fileList.firstFileToUpload] : []; } if (filesToUpload && filesToUpload.length > 0) { this._uploadFiles(filesToUpload); } } retryFiles(uid) { const filesToRetry = [this.fileList.get(uid)]; if (filesToRetry) { this._uploadFiles(filesToRetry); } } _uploadFiles(allFiles) { for (const filesToUpload of allFiles) { if (filesToUpload[0].state === FileState.Paused) { return; } // Clone the Headers so that the default ones are not overridden const uploadEventArgs = new UploadEvent(filesToUpload, this.cloneRequestHeaders(this.async.saveHeaders)); this.uploadEvent.emit(uploadEventArgs); if (!uploadEventArgs.isDefaultPrevented()) { this.fileList.setFilesState(filesToUpload, FileState.Uploading); const httpSubcription = this.performUpload(filesToUpload, uploadEventArgs); filesToUpload.forEach((file) => { file.httpSubscription = httpSubcription; }); } else { this.fileList.remove(filesToUpload[0].uid); this.checkAllComplete(); } } } performRemove(files, removeEventArgs) { const async = this.async; const fileNames = files.map((file) => { return file.name; }); const formData = this.populateRemoveFormData(fileNames, removeEventArgs.data); const options = this.populateRequestOptions(removeEventArgs.headers, false); const removeRequest = new HttpRequest(async.removeMethod, async.removeUrl, formData, options); this.http.request(removeRequest) .subscribe(success => { this.onSuccess(success, files, "remove"); }, error => { this.onError(error, files, "remove"); }); } performUpload(files, uploadEventArgs) { const async = this.async; const formData = this.populateUploadFormData(files, uploadEventArgs.data); const options = this.populateRequestOptions(uploadEventArgs.headers); const uploadRequest = new HttpRequest(async.saveMethod, async.saveUrl, formData, options); const httpSubscription = this.http.request(uploadRequest) .subscribe(event => { if (event.type === HttpEventType.UploadProgress && !this.async.chunk) { this.onProgress(event, files); } else if (event instanceof HttpResponse) { this.onSuccess(event, files, "upload"); this.checkAllComplete(); } }, error => { this.onError(error, files, "upload"); this.checkAllComplete(); }); return httpSubscription; } onSuccess(successResponse, files, operation) { if (operation === "upload" && this.async.chunk) { this.onChunkProgress(files); if (this.isChunkUploadComplete(files[0].uid)) { this.removeChunkInfo(files[0].uid); } else { this.updateChunkInfo(files[0].uid); this._uploadFiles([files]); return; } } const successArgs = new SuccessEvent(files, operation, successResponse); this.successEvent.emit(successArgs); if (operation === "upload") { this.fileList.setFilesState(files, successArgs.isDefaultPrevented() ? FileState.Failed : FileState.Uploaded); } else { if (!successArgs.isDefaultPrevented()) { this.fileList.remove(files[0].uid); } } if (!successArgs.isDefaultPrevented()) { this.onChange(); } } onError(errorResponse, files, operation) { if (operation === "upload" && this.async.chunk) { const maxRetries = this.chunk.maxAutoRetries; const chunkInfo = this.chunkMap.get(files[0].uid); if (chunkInfo.retries < maxRetries) { chunkInfo.retries += 1; setTimeout(() => { this.retryFiles(files[0].uid); }, this.chunk.autoRetryAfter); return; } } const errorArgs = new ErrorEvent(files, operation, errorResponse); this.errorEvent.emit(errorArgs); if (operation === "upload") { this.fileList.setFilesState(files, FileState.Failed); } } onProgress(event, files) { const percentComplete = Math.round(100 * event.loaded / event.total); const progressArgs = new UploadProgressEvent(files, percentComplete < 100 ? percentComplete : 100); this.uploadProgressEvent.emit(progressArgs); } onChunkProgress(files) { const chunkInfo = this.chunkMap.get(files[0].uid); let percentComplete = 0; if (chunkInfo) { if (chunkInfo.index === chunkInfo.totalChunks - 1) { percentComplete = 100; } else { percentComplete = Math.round(((chunkInfo.index + 1) / chunkInfo.totalChunks) * 100); } } const progressArgs = new UploadProgressEvent(files, percentComplete < 100 ? percentComplete : 100); this.uploadProgressEvent.emit(progressArgs); } checkAllComplete() { if (!this.fileList.hasFileWithState([ FileState.Uploading, FileState.Paused ]) && this.areAllSelectedFilesHandled()) { this.completeEvent.emit(); } else if (this.shouldUploadNextFile()) { this.uploadFiles(); } } shouldUploadNextFile() { return !this.async.concurrent && this.fileList.hasFileWithState([FileState.Selected]) && !this.fileList.hasFileWithState([FileState.Uploading]); } areAllSelectedFilesHandled() { const validSelectedFiles = this.fileList.getFilesWithState(FileState.Selected).filter(file => !file.validationErrors); return validSelectedFiles.length === 0; } cloneRequestHeaders(headers) { const cloned = {}; if (headers) { headers.keys().forEach((key) => { if (key !== 'constructor' && key !== '__proto__' && key !== 'prototype') cloned[key] = headers.get(key); }); } return new HttpHeaders(cloned); } populateRequestOptions(headers, reportProgress = true) { return { headers: headers, reportProgress: reportProgress, responseType: this.async.responseType, withCredentials: this.async.withCredentials }; } populateUploadFormData(files, clientData) { const saveField = this.async.saveField; const data = new FormData(); this.populateClientFormData(data, clientData); if (this.async.chunk) { data.append(saveField, this.getNextChunk(files[0])); data.append("metadata", this.getChunkMetadata(files[0])); } else { for (const file of files) { data.append(saveField, file.rawFile); } } return data; } populateRemoveFormData(fileNames, clientData) { const data = new FormData(); this.populateClientFormData(data, clientData); for (const fileName of fileNames) { data.append(this.async.removeField, fileName); } return data; } populateClientFormData(data, clientData) { for (const key in clientData) { if (clientData.hasOwnProperty(key)) { data.append(key, clientData[key]); } } } /* Chunking Helper Methods Section */ getNextChunk(file) { const info = this.getChunkInfo(file); const newPosition = info.position + this.chunk.size; return file.rawFile.slice(info.position, newPosition); } getChunkInfo(file) { let chunkInfo = this.chunkMap.get(file.uid); if (!chunkInfo) { const totalChunks = file.size > 0 ? Math.ceil(file.size / this.chunk.size) : 1; chunkInfo = this.chunkMap.add(file.uid, totalChunks); } return chunkInfo; } updateChunkInfo(uid) { const chunkInfo = this.chunkMap.get(uid); if (chunkInfo.index < chunkInfo.totalChunks - 1) { chunkInfo.index += 1; chunkInfo.position += this.chunk.size; chunkInfo.retries = 0; } } removeChunkInfo(uid) { this.chunkMap.remove(uid); } getChunkMetadata(file) { const chunkInfo = this.chunkMap.get(file.uid); const chunkMetadata = { chunkIndex: chunkInfo.index, contentType: file.rawFile.type, fileName: file.name, fileSize: file.size, fileUid: file.uid, totalChunks: chunkInfo.totalChunks }; return JSON.stringify(chunkMetadata); } isChunkUploadComplete(uid) { const chunkInfo = this.chunkMap.get(uid); if (chunkInfo) { return chunkInfo.index + 1 === chunkInfo.totalChunks; } return false; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: UploadService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: UploadService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: UploadService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: i1.HttpClient }]; } }); /** * @hidden */ class NavigationService { uploadService; zone; onActionButtonFocus = new EventEmitter(); onFileAction = new EventEmitter(); onFileFocus = new EventEmitter(); onTabOut = new EventEmitter(); onWrapperFocus = new EventEmitter(); onSelectButtonFocus = new EventEmitter(); actionButtonsVisible = false; fileListVisible = false; focused = false; keyBindings; focusedFileIndex = 0; _focusedIndex = -1; constructor(uploadService, zone) { this.uploadService = uploadService; this.zone = zone; } action(event) { const key = event.keyCode; return this.keyBindings[key]; } process(event, component) { const handler = this.action(event); if (handler) { handler(event, component); } } computeKeys() { this.keyBindings = { [Keys.Space]: () => this.handleSpace(), [Keys.Enter]: () => this.handleEnter(), [Keys.Escape]: () => this.handleEscape(), [Keys.Delete]: () => this.handleDelete(), [Keys.Tab]: (event, component) => this.handleTab(event, component), [Keys.ArrowUp]: (event) => this.handleUpDown(event, -1), [Keys.ArrowDown]: (event) => this.handleUpDown(event, 1) }; } focusSelectButton() { this.focused = true; this._focusedIndex = -1; this.onSelectButtonFocus.emit(); } handleEnter() { if (this.lastIndex >= 0 && this.focusedIndex >= 0 && this.focusedIndex <= this.lastFileIndex) { this.zone.run(() => this.onFileAction.emit(Keys.Enter)); } } handleSpace() { if (this.lastIndex >= 0 && this.focusedIndex >= 0 && this.focusedIndex <= this.lastFileIndex) { this.zone.run(() => this.onFileAction.emit(Keys.Space)); } } handleDelete() { if (this.focusedIndex >= 0 && this.focusedIndex <= this.lastFileIndex) { this.zone.run(() => this.onFileAction.emit(Keys.Delete)); } } handleEscape() { if (this.focusedIndex >= 0 && this.focusedIndex <= this.lastFileIndex) { this.zone.run(() => this.onFileAction.emit(Keys.Escape)); } } handleTab(event, component) { const shifted = event.shiftKey; /* Select Files button is focused */ if (this.focusedIndex === -1 && this.fileListVisible && !shifted) { this.focusedIndex = this.focusedFileIndex; event.preventDefault(); this.onFileFocus.emit(this.focusedFileIndex); return; } /* File in the list is focused */ if (this.focusedIndex > -1 && this.focusedIndex <= this.lastFileIndex) { if (shifted) { this.focusedIndex = -1; } else if (component !== 'fileselect' && this.actionButtonsVisible) { this.focusedIndex = this.lastFileIndex + 1; return; } } /* Clear button is focused */ if (this.focusedIndex === this.lastFileIndex + 1) { this.focusedIndex = shifted ? this.focusedFileIndex : this.lastIndex; if (shifted) { event.preventDefault(); this.onFileFocus.emit(this.focusedFileIndex); } return; } /* Upload button is focused */ if (this.focusedIndex === this.lastIndex && this.actionButtonsVisible && shifted) { this.focusedIndex -= 1; return; } this.onTabOut.emit(); } handleUpDown(event, direction) { const focusOnFileList = this.focusedIndex > -1 && this.uploadService.files.count >= 0; const nextFocusableIndexInBoundaries = direction > 0 ? this.focusedFileIndex < this.lastFileIndex : this.focusedFileIndex > 0; const focusNextFile = focusOnFileList && nextFocusableIndexInBoundaries; if (focusNextFile) { event.preventDefault(); this.zone.run(() => { this.focusedIndex += direction; this.focusedFileIndex += direction; }); } } get focusedIndex() { return this._focusedIndex; } set focusedIndex(index) { if (!this.focused) { this.onWrapperFocus.emit(); } this._focusedIndex = index; this.focused = true; if (this._focusedIndex >= 0 && this._focusedIndex <= this.lastFileIndex) { this.onFileFocus.emit(index); } } get lastFileIndex() { return this.actionButtonsVisible ? this.lastIndex - 2 : this.lastIndex; } get lastIndex() { const fileCount = this.uploadService.files.count; return this.actionButtonsVisible ? fileCount + 1 : fileCount - 1; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavigationService, deps: [{ token: UploadService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavigationService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavigationService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: UploadService }, { type: i0.NgZone }]; } }); const components = {}; /** * @hidden */ class DropZoneService { addComponent(component, zoneId) { if (this.has(zoneId)) { components[zoneId].push(component); } else { components[zoneId] = [component]; } } clearComponent(component, zoneId) { if (this.has(zoneId)) { const componentIdx = components[zoneId].indexOf(component); components[zoneId].splice(componentIdx, 1); } } getComponents(zoneId) { return components[zoneId]; } has(id) { return id in components; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DropZoneService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DropZoneService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DropZoneService, decorators: [{ type: Injectable }] }); /** * @hidden */ const INVALIDMAXFILESIZE = "invalidMaxFileSize"; /** * @hidden */ const INVALIDMINFILESIZE = "invalidMinFileSize"; /** * @hidden */ const INVALIDFILEEXTENSION = "invalidFileExtension"; const validateFileExtension = (file, allowedExtensions) => { if (allowedExtensions.length > 0) { if (allowedExtensions.indexOf(file.extension.toLowerCase()) < 0) { file.validationErrors = file.validationErrors || []; if (file.validationErrors.indexOf(INVALIDFILEEXTENSION) < 0) { file.validationErrors.push(INVALIDFILEEXTENSION); } } } }; const validateFileSize = (file, minFileSize, maxFileSize) => { if (minFileSize !== 0 && file.size < minFileSize) { file.validationErrors = file.validationErrors || []; if (file.validationErrors.indexOf(INVALIDMINFILESIZE) < 0) { file.validationErrors.push(INVALIDMINFILESIZE); } } if (maxFileSize !== 0 && file.size > maxFileSize) { file.validationErrors = file.validationErrors || []; if (file.validationErrors.indexOf(INVALIDMAXFILESIZE) < 0) { file.validationErrors.push(INVALIDMAXFILESIZE); } } }; const parseAllowedExtensions = (extensions) => { const allowedExtensions = extensions.map((ext) => { const parsedExt = (ext.substring(0, 1) === ".") ? ext : ("." + ext); return parsedExt.toLowerCase(); }); return allowedExtensions; }; /** * @hidden */ const validateFiles = (files, restrictionInfo) => { const allowedExtensions = parseAllowedExtensions(restrictionInfo.allowedExtensions); const maxFileSize = restrictionInfo.maxFileSize; const minFileSize = restrictionInfo.minFileSize; let i; for (i = 0; i < files.length; i++) { validateFileExtension(files[i], allowedExtensions); validateFileSize(files[i], minFileSize, maxFileSize); } }; /** * @hidden */ const packageMetadata = { name: '@progress/kendo-angular-upload', productName: 'Kendo UI for Angular', productCode: 'KENDOUIANGULAR', productCodes: ['KENDOUIANGULAR'], publishDate: 1750770824, version: '19.1.2', licensingDocsUrl: 'https://www.telerik.com/kendo-angular-ui/my-license/' }; /** * Customizes the rendering of files in the list. ([See example.](slug:templates_upload#toc-file-template)) * * The following context variables are available in the template: * * * `let-files`&mdash;A reference to the files associated with the current item. * * `let-state`&mdash;A reference to the current state of each file. If the [`batch`](slug:api_upload_uploadcomponent#toc-batch) option of the Upload is set to `true`, the field reflects the state of the whole batch. * * `#myUpload="kendoUpload"` or `#myFileSelect="kendoFileSelect"`&mdash;A reference to the instance